Compare commits

..

47 Commits

Author SHA1 Message Date
arvinxx 4fa6d64df2 add plugin settings 2025-07-29 14:48:46 +08:00
Zhijie He 403aebd52e 💄 style: add more OpenAI SDK Text2Image providers (#8573) 2025-07-29 14:45:19 +08:00
LobeHub Bot 356cf0c392 💄 style: update i18n (#8593)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-29 14:37:29 +08:00
Arvin Xu be98d56ef4 ♻️ refactor: refactor jose-JWT to xor obfuscation (#8595)
* refactor jose-jwt to xor obfuscation

* rename JWTPayload type to ClientSecretPayload

* fix tests

* revert next version
2025-07-29 14:37:01 +08:00
lobehubbot 3fae1b2638 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-29 04:07:54 +00:00
semantic-release-bot ee4cc6c2e0 🔖 chore(release): v1.105.1 [skip ci]
### [Version&nbsp;1.105.1](https://github.com/lobehub/lobe-chat/compare/v1.105.0...v1.105.1)
<sup>Released on **2025-07-29**</sup>

#### 💄 Styles

- **misc**: Support more Text2Image from Qwen.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### Styles

* **misc**: Support more Text2Image from Qwen, closes [#8574](https://github.com/lobehub/lobe-chat/issues/8574) ([b8c0e2d](https://github.com/lobehub/lobe-chat/commit/b8c0e2d))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-29 04:07:02 +00:00
Zhijie He b8c0e2d639 💄 style: support more Text2Image from Qwen (#8574) 2025-07-29 11:51:40 +08:00
lobehubbot 74d20bdbe8 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 09:12:32 +00:00
semantic-release-bot 7bab44e74c 🔖 chore(release): v1.105.0 [skip ci]
## [Version&nbsp;1.105.0](https://github.com/lobehub/lobe-chat/compare/v1.104.5...v1.105.0)
<sup>Released on **2025-07-28**</sup>

####  Features

- **misc**: Implement API Key management functionality.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's improved

* **misc**: Implement API Key management functionality, closes [#8535](https://github.com/lobehub/lobe-chat/issues/8535) ([fdaa725](https://github.com/lobehub/lobe-chat/commit/fdaa725))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 09:11:38 +00:00
Zephyr fdaa72564c feat: Implement API Key management functionality (#8535)
*  feat: Implement API Key management functionality

- Added new components for API Key management including creation, deletion, and display.
- Introduced a new database schema for storing API Keys.
- Implemented server and client services for API Key operations.
- Integrated API Key management into the profile section with appropriate routing and feature flags.
- Enhanced localization support for API Key related UI elements.

This commit lays the groundwork for managing API Keys within the application, allowing users to create, view, and manage their keys effectively.

* fix: server config unit test

*  feat(database): Create api_keys table with conditional existence check

- Added a conditional check to create the "api_keys" table only if it does not already exist.
- Ensured the foreign key constraint for "user_id" references the "users" table remains intact.

This change enhances the migration process by preventing errors during table creation if the table already exists.

* feat: Implement API Key management interface

- Introduced a new Client component for managing API keys, including creation, updating, and deletion functionalities.
- Replaced the previous page component with the new Client component in the API key management page.
- Removed obsolete client and server service files related to API key management, streamlining the service layer.

This update enhances the user experience by providing a dedicated interface for API key operations.
2025-07-28 16:55:57 +08:00
lobehubbot 7d85151cb6 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 08:53:48 +00:00
semantic-release-bot 23ef2eea59 🔖 chore(release): v1.104.5 [skip ci]
### [Version&nbsp;1.104.5](https://github.com/lobehub/lobe-chat/compare/v1.104.4...v1.104.5)
<sup>Released on **2025-07-28**</sup>

#### 💄 Styles

- **misc**: Fix setting window layout when in desktop was disappear.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### Styles

* **misc**: Fix setting window layout when in desktop was disappear, closes [#8585](https://github.com/lobehub/lobe-chat/issues/8585) ([74ab822](https://github.com/lobehub/lobe-chat/commit/74ab822))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 08:52:59 +00:00
Shinji-Li 74ab822140 💄 style: fix setting window layout when in desktop was disappear (#8585) 2025-07-28 16:37:45 +08:00
lobehubbot d726ff108d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 02:37:48 +00:00
semantic-release-bot dd7b661140 🔖 chore(release): v1.104.4 [skip ci]
### [Version&nbsp;1.104.4](https://github.com/lobehub/lobe-chat/compare/v1.104.3...v1.104.4)
<sup>Released on **2025-07-28**</sup>

#### 💄 Styles

- **misc**: Fix setting window layout size, update i18n.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### Styles

* **misc**: Fix setting window layout size, closes [#8483](https://github.com/lobehub/lobe-chat/issues/8483) ([4902341](https://github.com/lobehub/lobe-chat/commit/4902341))
* **misc**: Update i18n, closes [#8579](https://github.com/lobehub/lobe-chat/issues/8579) ([2eccbc7](https://github.com/lobehub/lobe-chat/commit/2eccbc7))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 02:36:53 +00:00
Shinji-Li 49023419cf 💄 style: fix setting window layout size (#8483) 2025-07-28 10:21:30 +08:00
LobeHub Bot 2eccbc79eb 💄 style: update i18n (#8579)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-28 10:19:46 +08:00
lobehubbot 3527cb65f1 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-26 09:48:50 +00:00
semantic-release-bot 1731c841d8 🔖 chore(release): v1.104.3 [skip ci]
### [Version&nbsp;1.104.3](https://github.com/lobehub/lobe-chat/compare/v1.104.2...v1.104.3)
<sup>Released on **2025-07-26**</sup>

#### 💄 Styles

- **misc**: Add Gemini 2.5 Flash-Lite GA model.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### Styles

* **misc**: Add Gemini 2.5 Flash-Lite GA model, closes [#8539](https://github.com/lobehub/lobe-chat/issues/8539) ([404ac21](https://github.com/lobehub/lobe-chat/commit/404ac21))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-26 09:47:57 +00:00
afon 404ac21229 💄 style: Add Gemini 2.5 Flash-Lite GA model (#8539) 2025-07-26 17:32:34 +08:00
Arvin Xu 2eaa2dbea0 🔨 chore: add react scan debugger and bump deps (#8576)
* add REACT_SCAN debug

* upgrade lobehub/ui

* clean

* head

* update
2025-07-26 16:32:55 +08:00
lobehubbot 50c0ed168d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-26 07:32:43 +00:00
semantic-release-bot 780e231afa 🔖 chore(release): v1.104.2 [skip ci]
### [Version&nbsp;1.104.2](https://github.com/lobehub/lobe-chat/compare/v1.104.1...v1.104.2)
<sup>Released on **2025-07-26**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix update hotkey invalid when input mod in desktop.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Fix update hotkey invalid when input mod in desktop, closes [#8572](https://github.com/lobehub/lobe-chat/issues/8572) ([07f3e6a](https://github.com/lobehub/lobe-chat/commit/07f3e6a))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-26 07:31:53 +00:00
Arvin Xu 07f3e6a4c4 🐛 fix: fix update hotkey invalid when input mod in desktop (#8572)
* fix hotkey with mod

* fix invalid hotkeys

* add tests
2025-07-26 15:16:45 +08:00
lobehubbot 3c35edced5 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-25 09:59:34 +00:00
semantic-release-bot 15770f188f 🔖 chore(release): v1.104.1 [skip ci]
### [Version&nbsp;1.104.1](https://github.com/lobehub/lobe-chat/compare/v1.104.0...v1.104.1)
<sup>Released on **2025-07-25**</sup>

#### 🐛 Bug Fixes

- **misc**: Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider, closes [#8557](https://github.com/lobehub/lobe-chat/issues/8557) ([d1e4a54](https://github.com/lobehub/lobe-chat/commit/d1e4a54))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-25 09:58:34 +00:00
YuTengjing 9d7c6014fd chore: improve image display quality (#8571) 2025-07-25 17:42:23 +08:00
sxjeru d1e4a54b01 🐛 fix: update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider (#8557) 2025-07-25 16:16:54 +08:00
lobehubbot 1bc8815fb4 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-24 17:20:43 +00:00
semantic-release-bot 284826bed0 🔖 chore(release): v1.104.0 [skip ci]
## [Version&nbsp;1.104.0](https://github.com/lobehub/lobe-chat/compare/v1.103.2...v1.104.0)
<sup>Released on **2025-07-24**</sup>

####  Features

- **misc**: Support custom hotkey on desktop.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's improved

* **misc**: Support custom hotkey on desktop, closes [#8559](https://github.com/lobehub/lobe-chat/issues/8559) ([b50f121](https://github.com/lobehub/lobe-chat/commit/b50f121))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-24 17:19:53 +00:00
Arvin Xu b50f1212cb feat: support custom hotkey on desktop (#8559)
* support custom hotkey

* update tests

* clean

* fix tests
2025-07-25 01:04:18 +08:00
lobehubbot 946517a52e 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-24 04:20:29 +00:00
semantic-release-bot d30cc62acf 🔖 chore(release): v1.103.2 [skip ci]
### [Version&nbsp;1.103.2](https://github.com/lobehub/lobe-chat/compare/v1.103.1...v1.103.2)
<sup>Released on **2025-07-24**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix chat stream in desktop and update shortcut.

#### 💄 Styles

- **misc**: Add cached token count to usage of GoogleAI and VertexAI, fix desktop titlebar style in window, fix sub topic width in md responsive.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Fix chat stream in desktop and update shortcut, closes [#8520](https://github.com/lobehub/lobe-chat/issues/8520) ([0192140](https://github.com/lobehub/lobe-chat/commit/0192140))

#### Styles

* **misc**: Add cached token count to usage of GoogleAI and VertexAI, closes [#8545](https://github.com/lobehub/lobe-chat/issues/8545) ([66dbb24](https://github.com/lobehub/lobe-chat/commit/66dbb24))
* **misc**: Fix desktop titlebar style in window, closes [#8439](https://github.com/lobehub/lobe-chat/issues/8439) ([fd7662c](https://github.com/lobehub/lobe-chat/commit/fd7662c))
* **misc**: Fix sub topic width in md responsive, closes [#8443](https://github.com/lobehub/lobe-chat/issues/8443) ([9bae13b](https://github.com/lobehub/lobe-chat/commit/9bae13b))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-24 04:19:17 +00:00
Arvin Xu 0192140909 🐛 fix: fix chat stream in desktop and update shortcut (#8520)
* disable nginx chunk buffering

* make setting keep alive

* update shortcut

* update shortcut

* fix lint
2025-07-24 12:04:06 +08:00
Shinji-Li 9bae13b6c1 💄 style: fix sub topic width in md responsive (#8443) 2025-07-24 11:02:38 +08:00
CanisMinor fd7662c3ac 💄 style: fix desktop titlebar style in window (#8439)
* 💄 style: Fix win electron style

📝 docs: Update readme

💄 style: Update useWatchThemeUpdate

💄 style: Update Tray icon

🔧 chore: Update windows

🔧 chore: Update filetree

🔧 chore: Update core

💄 style: Fix desktop draw style

💄 style: Update style

💄 style: Fix backgroundColor

💄 style: Update titlebar style

* 💄 style: Fix windows icon

* 🔧 chore: Clean

* update theme

* 💄 style: Update broswer

* 💄 style: HandleAppThemeChange

* clean

* fix memory leak

---------

Co-authored-by: arvinxx <arvinx@foxmail.com>
2025-07-24 11:02:07 +08:00
afon 66dbb246d9 💄 style: Add cached token count to usage of GoogleAI and VertexAI (#8545) 2025-07-24 10:56:03 +08:00
Jamie Stivala 3a2935a6e3 🔨 chore: fix an issue where some tests were failing if Branding items were updated (#8549)
* Update test data for plugin action to use avatar icon path (rather than hard coded)

* Update tests to use BRANDING_NAME constant instead of hardcoded 'LobeChat' and update avatar icon path in chat message tests

* Update tests to replace hardcoded avatar paths with constants for inbox and user avatars

* Update plugin action tests to use DEFAULT_INBOX_AVATAR constant instead of hardcoded path
2025-07-24 10:54:42 +08:00
YuTengjing 82ca0074d4 🔨 chore: some ai image optimization (#8543) 2025-07-23 15:33:20 +08:00
lobehubbot 225d3b6ed5 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-23 05:34:17 +00:00
semantic-release-bot 66984d9418 🔖 chore(release): v1.103.1 [skip ci]
### [Version&nbsp;1.103.1](https://github.com/lobehub/lobe-chat/compare/v1.103.0...v1.103.1)
<sup>Released on **2025-07-23**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### Styles

* **misc**: Update i18n, closes [#8537](https://github.com/lobehub/lobe-chat/issues/8537) ([b16f19b](https://github.com/lobehub/lobe-chat/commit/b16f19b))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-23 05:33:24 +00:00
LobeHub Bot b16f19bfed 💄 style: update i18n (#8537)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-23 13:17:56 +08:00
huangkairan f61b222e9e 📝 docs: fix desktop & mcp mdx (#8541) 2025-07-23 13:17:30 +08:00
lobehubbot 368f7dbbc1 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-22 14:59:13 +00:00
semantic-release-bot 9c2350e643 🔖 chore(release): v1.103.0 [skip ci]
## [Version&nbsp;1.103.0](https://github.com/lobehub/lobe-chat/compare/v1.102.4...v1.103.0)
<sup>Released on **2025-07-22**</sup>

####  Features

- **misc**: Add Qwen image generation capabilities.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's improved

* **misc**: Add Qwen image generation capabilities, closes [#8534](https://github.com/lobehub/lobe-chat/issues/8534) ([7e8e5ef](https://github.com/lobehub/lobe-chat/commit/7e8e5ef))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-22 14:58:22 +00:00
YuTengjing 7e8e5ef5b2 feat: add Qwen image generation capabilities (#8534) 2025-07-22 22:43:10 +08:00
lobehubbot 0ed9e7d947 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-22 06:02:40 +00:00
264 changed files with 15920 additions and 1412 deletions
+2
View File
@@ -26,6 +26,8 @@ Gather the modified code and context. Please strictly follow the process below:
### Code Style
read [typescript.mdc](mdc:.cursor/rules/typescript.mdc) to learn the project's code style.
- Ensure JSDoc comments accurately reflect the implementation; update them when needed.
- Look for opportunities to simplify or modernize code with the latest JavaScript/TypeScript features.
- Prefer `async`/`await` over callbacks or chained `.then` promises.
+3 -1
View File
@@ -16,4 +16,6 @@ TypeScript Code Style Guide:
- Always refactor repeated logic into a reusable function
- Don't remove meaningful code comments, be sure to keep original comments when providing applied code
- Update the code comments when needed after you modify the related code
- Please respect my prettier preferences when you provide code
- Please respect my prettier preferences when you provide code
- Prefer object destructuring when accessing and using properties
- Prefer async version api than sync version, eg: use readFile from 'fs/promises' instead of 'fs'
+5 -6
View File
@@ -41,10 +41,6 @@ test-output
# husky
.husky/prepare-commit-msg
# misc
# add other ignore file below
CLAUDE.md
# local env files
.env*.local
@@ -74,5 +70,8 @@ vertex-ai-key.json
./packages/lobe-ui
# for local prd docs
docs/prd
# local use ai coding files
docs/.prd
.claude
.mcp.json
CLAUDE.md
+286
View File
@@ -2,6 +2,292 @@
# Changelog
### [Version 1.105.1](https://github.com/lobehub/lobe-chat/compare/v1.105.0...v1.105.1)
<sup>Released on **2025-07-29**</sup>
#### 💄 Styles
- **misc**: Support more Text2Image from Qwen.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Support more Text2Image from Qwen, closes [#8574](https://github.com/lobehub/lobe-chat/issues/8574) ([b8c0e2d](https://github.com/lobehub/lobe-chat/commit/b8c0e2d))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.105.0](https://github.com/lobehub/lobe-chat/compare/v1.104.5...v1.105.0)
<sup>Released on **2025-07-28**</sup>
#### ✨ Features
- **misc**: Implement API Key management functionality.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Implement API Key management functionality, closes [#8535](https://github.com/lobehub/lobe-chat/issues/8535) ([fdaa725](https://github.com/lobehub/lobe-chat/commit/fdaa725))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.104.5](https://github.com/lobehub/lobe-chat/compare/v1.104.4...v1.104.5)
<sup>Released on **2025-07-28**</sup>
#### 💄 Styles
- **misc**: Fix setting window layout when in desktop was disappear.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Fix setting window layout when in desktop was disappear, closes [#8585](https://github.com/lobehub/lobe-chat/issues/8585) ([74ab822](https://github.com/lobehub/lobe-chat/commit/74ab822))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.104.4](https://github.com/lobehub/lobe-chat/compare/v1.104.3...v1.104.4)
<sup>Released on **2025-07-28**</sup>
#### 💄 Styles
- **misc**: Fix setting window layout size, update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Fix setting window layout size, closes [#8483](https://github.com/lobehub/lobe-chat/issues/8483) ([4902341](https://github.com/lobehub/lobe-chat/commit/4902341))
- **misc**: Update i18n, closes [#8579](https://github.com/lobehub/lobe-chat/issues/8579) ([2eccbc7](https://github.com/lobehub/lobe-chat/commit/2eccbc7))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.104.3](https://github.com/lobehub/lobe-chat/compare/v1.104.2...v1.104.3)
<sup>Released on **2025-07-26**</sup>
#### 💄 Styles
- **misc**: Add Gemini 2.5 Flash-Lite GA model.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Add Gemini 2.5 Flash-Lite GA model, closes [#8539](https://github.com/lobehub/lobe-chat/issues/8539) ([404ac21](https://github.com/lobehub/lobe-chat/commit/404ac21))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.104.2](https://github.com/lobehub/lobe-chat/compare/v1.104.1...v1.104.2)
<sup>Released on **2025-07-26**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix update hotkey invalid when input mod in desktop.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix update hotkey invalid when input mod in desktop, closes [#8572](https://github.com/lobehub/lobe-chat/issues/8572) ([07f3e6a](https://github.com/lobehub/lobe-chat/commit/07f3e6a))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.104.1](https://github.com/lobehub/lobe-chat/compare/v1.104.0...v1.104.1)
<sup>Released on **2025-07-25**</sup>
#### 🐛 Bug Fixes
- **misc**: Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider, closes [#8557](https://github.com/lobehub/lobe-chat/issues/8557) ([d1e4a54](https://github.com/lobehub/lobe-chat/commit/d1e4a54))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.104.0](https://github.com/lobehub/lobe-chat/compare/v1.103.2...v1.104.0)
<sup>Released on **2025-07-24**</sup>
#### ✨ Features
- **misc**: Support custom hotkey on desktop.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Support custom hotkey on desktop, closes [#8559](https://github.com/lobehub/lobe-chat/issues/8559) ([b50f121](https://github.com/lobehub/lobe-chat/commit/b50f121))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.103.2](https://github.com/lobehub/lobe-chat/compare/v1.103.1...v1.103.2)
<sup>Released on **2025-07-24**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix chat stream in desktop and update shortcut.
#### 💄 Styles
- **misc**: Add cached token count to usage of GoogleAI and VertexAI, fix desktop titlebar style in window, fix sub topic width in md responsive.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix chat stream in desktop and update shortcut, closes [#8520](https://github.com/lobehub/lobe-chat/issues/8520) ([0192140](https://github.com/lobehub/lobe-chat/commit/0192140))
#### Styles
- **misc**: Add cached token count to usage of GoogleAI and VertexAI, closes [#8545](https://github.com/lobehub/lobe-chat/issues/8545) ([66dbb24](https://github.com/lobehub/lobe-chat/commit/66dbb24))
- **misc**: Fix desktop titlebar style in window, closes [#8439](https://github.com/lobehub/lobe-chat/issues/8439) ([fd7662c](https://github.com/lobehub/lobe-chat/commit/fd7662c))
- **misc**: Fix sub topic width in md responsive, closes [#8443](https://github.com/lobehub/lobe-chat/issues/8443) ([9bae13b](https://github.com/lobehub/lobe-chat/commit/9bae13b))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.103.1](https://github.com/lobehub/lobe-chat/compare/v1.103.0...v1.103.1)
<sup>Released on **2025-07-23**</sup>
#### 💄 Styles
- **misc**: Update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Update i18n, closes [#8537](https://github.com/lobehub/lobe-chat/issues/8537) ([b16f19b](https://github.com/lobehub/lobe-chat/commit/b16f19b))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.103.0](https://github.com/lobehub/lobe-chat/compare/v1.102.4...v1.103.0)
<sup>Released on **2025-07-22**</sup>
#### ✨ Features
- **misc**: Add Qwen image generation capabilities.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Add Qwen image generation capabilities, closes [#8534](https://github.com/lobehub/lobe-chat/issues/8534) ([7e8e5ef](https://github.com/lobehub/lobe-chat/commit/7e8e5ef))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.102.4](https://github.com/lobehub/lobe-chat/compare/v1.102.3...v1.102.4)
<sup>Released on **2025-07-22**</sup>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 171 KiB

+4 -2
View File
@@ -11,8 +11,9 @@ console.log(`[electron-vite.config.ts] Detected UPDATE_CHANNEL: ${updateChannel}
export default defineConfig({
main: {
build: {
minify: !isDev,
outDir: 'dist/main',
sourcemap: isDev,
sourcemap: isDev ? 'inline' : false,
},
// 这里是关键:在构建时进行文本替换
define: {
@@ -30,8 +31,9 @@ export default defineConfig({
},
preload: {
build: {
minify: !isDev,
outDir: 'dist/preload',
sourcemap: isDev,
sourcemap: isDev ? 'inline' : false,
},
plugins: [externalizeDepsPlugin({})],
resolve: {
+1
View File
@@ -31,6 +31,7 @@
},
"dependencies": {
"electron-updater": "^6.6.2",
"electron-window-state": "^5.0.3",
"get-port-please": "^3.1.2",
"pdfjs-dist": "4.10.38"
},
+2 -2
View File
@@ -1,4 +1,4 @@
import type { BrowserWindowOpts } from './core/Browser';
import type { BrowserWindowOpts } from './core/browser/Browser';
export const BrowsersIdentifiers = {
chat: 'chat',
@@ -36,7 +36,7 @@ export const appBrowsers = {
autoHideMenuBar: true,
height: 800,
identifier: 'settings',
// keepAlive: true,
keepAlive: true,
minWidth: 600,
parentIdentifier: 'chat',
path: '/settings',
+5 -4
View File
@@ -1,12 +1,13 @@
import { dev, linux, macOS, windows } from 'electron-is';
import os from 'node:os';
export const isDev = process.env.NODE_ENV === 'development';
export const isDev = dev();
export const OFFICIAL_CLOUD_SERVER = process.env.OFFICIAL_CLOUD_SERVER || 'https://lobechat.com';
export const isMac = process.platform === 'darwin';
export const isWindows = process.platform === 'win32';
export const isLinux = process.platform === 'linux';
export const isMac = macOS();
export const isWindows = windows();
export const isLinux = linux();
function getIsWindows11() {
if (!isWindows) return false;
+1
View File
@@ -31,4 +31,5 @@ export const STORE_DEFAULTS: ElectronMainStore = {
networkProxy: defaultProxySettings,
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
storagePath: appStorageDir,
themeMode: 'auto',
};
+11
View File
@@ -0,0 +1,11 @@
// Theme colors
export const BACKGROUND_DARK = '#000';
export const BACKGROUND_LIGHT = '#f8f8f8';
export const SYMBOL_COLOR_DARK = '#ffffff80';
export const SYMBOL_COLOR_LIGHT = '#00000080';
// Window dimensions and constraints
export const TITLE_BAR_HEIGHT = 29;
// Default window configuration
export const THEME_CHANGE_DELAY = 100;
@@ -7,7 +7,7 @@ import { IpcClientEventSender } from '@/types/ipcClientEvent';
import { ControllerModule, ipcClientEvent, shortcut } from './index';
export default class BrowserWindowsCtr extends ControllerModule {
@shortcut('toggleMainWindow')
@shortcut('showApp')
async toggleMainWindow() {
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.toggleVisible();
@@ -77,10 +77,8 @@ export default class NotificationCtr extends ControllerModule {
const notification = new Notification({
body: params.body,
// 添加更多配置以确保通知能正常显示
hasReply: false,
silent: params.silent || false,
hasReply: false,
silent: params.silent || false,
timeoutType: 'default',
title: params.title,
urgency: 'normal',
@@ -1,3 +1,5 @@
import { ShortcutUpdateResult } from '@/core/ui/ShortcutManager';
import { ControllerModule, ipcClientEvent } from '.';
export default class ShortcutController extends ControllerModule {
@@ -13,7 +15,13 @@ export default class ShortcutController extends ControllerModule {
* 更新单个快捷键配置
*/
@ipcClientEvent('updateShortcutConfig')
updateShortcutConfig(id: string, accelerator: string): boolean {
updateShortcutConfig({
id,
accelerator,
}: {
accelerator: string;
id: string;
}): ShortcutUpdateResult {
return this.app.shortcutManager.updateShortcutConfig(id, accelerator);
}
}
@@ -83,7 +83,11 @@ export default class SystemController extends ControllerModule {
@ipcClientEvent('updateThemeMode')
async updateThemeModeHandler(themeMode: ThemeMode) {
this.app.storeManager.set('themeMode', themeMode);
this.app.browserManager.broadcastToAllWindows('themeChanged', { themeMode });
// Apply visual effects to all browser windows when theme mode changes
this.app.browserManager.handleAppThemeChange();
}
@ipcServerEvent('getDatabasePath')
@@ -6,16 +6,12 @@ import {
import { createLogger } from '@/utils/logger';
import { ControllerModule, ipcClientEvent, shortcut } from './index';
import { ControllerModule, ipcClientEvent } from './index';
// 创建日志记录器
const logger = createLogger('controllers:TrayMenuCtr');
export default class TrayMenuCtr extends ControllerModule {
/**
* 使用快捷键切换窗口可见性
*/
@shortcut('toggleMainWindow')
async toggleMainWindow() {
logger.debug('通过快捷键切换主窗口可见性');
const mainWindow = this.app.browserManager.getMainWindow();
@@ -47,7 +43,7 @@ export default class TrayMenuCtr extends ControllerModule {
return {
error: '托盘通知仅在 Windows 平台支持',
success: false
success: false,
};
}
@@ -71,7 +67,7 @@ export default class TrayMenuCtr extends ControllerModule {
logger.error('更新托盘图标失败:', error);
return {
error: String(error),
success: false
success: false,
};
}
}
@@ -79,7 +75,7 @@ export default class TrayMenuCtr extends ControllerModule {
return {
error: '托盘功能仅在 Windows 平台支持',
success: false
success: false,
};
}
@@ -103,7 +99,7 @@ export default class TrayMenuCtr extends ControllerModule {
return {
error: '托盘功能仅在 Windows 平台支持',
success: false
success: false,
};
}
}
@@ -5,9 +5,9 @@ import type { App } from '@/core/App';
import ShortcutController from '../ShortcutCtr';
// 模拟 App 及其依赖项
const mockGetShortcutsConfig = vi.fn().mockReturnValue({
const mockGetShortcutsConfig = vi.fn().mockReturnValue({
toggleMainWindow: 'CommandOrControl+Shift+L',
openSettings: 'CommandOrControl+,'
openSettings: 'CommandOrControl+,',
});
const mockUpdateShortcutConfig = vi.fn().mockImplementation((id, accelerator) => {
// 简单模拟更新成功
@@ -32,11 +32,11 @@ describe('ShortcutController', () => {
describe('getShortcutsConfig', () => {
it('should return shortcuts config from shortcutManager', () => {
const result = shortcutController.getShortcutsConfig();
expect(mockGetShortcutsConfig).toHaveBeenCalled();
expect(result).toEqual({
toggleMainWindow: 'CommandOrControl+Shift+L',
openSettings: 'CommandOrControl+,'
openSettings: 'CommandOrControl+,',
});
});
});
@@ -45,9 +45,9 @@ describe('ShortcutController', () => {
it('should call shortcutManager.updateShortcutConfig with correct parameters', () => {
const id = 'toggleMainWindow';
const accelerator = 'CommandOrControl+Alt+L';
const result = shortcutController.updateShortcutConfig(id, accelerator);
const result = shortcutController.updateShortcutConfig({ id, accelerator });
expect(mockUpdateShortcutConfig).toHaveBeenCalledWith(id, accelerator);
expect(result).toBe(true);
});
@@ -55,10 +55,13 @@ describe('ShortcutController', () => {
it('should return the result from shortcutManager.updateShortcutConfig', () => {
// 模拟更新失败的情况
mockUpdateShortcutConfig.mockReturnValueOnce(false);
const result = shortcutController.updateShortcutConfig('invalidKey', 'invalid+combo');
const result = shortcutController.updateShortcutConfig({
id: 'invalidKey',
accelerator: 'invalid+combo',
});
expect(result).toBe(false);
});
});
});
});
+1 -1
View File
@@ -2,7 +2,7 @@ import type { ClientDispatchEvents } from '@lobechat/electron-client-ipc';
import type { ServerDispatchEvents } from '@lobechat/electron-server-ipc';
import type { App } from '@/core/App';
import { IoCContainer } from '@/core/IoCContainer';
import { IoCContainer } from '@/core/infrastructure/IoCContainer';
import { ShortcutActionType } from '@/shortcuts';
const ipcDecorator =
+9 -10
View File
@@ -9,20 +9,19 @@ import { buildDir, nextStandaloneDir } from '@/const/dir';
import { isDev } from '@/const/env';
import { IControlModule } from '@/controllers';
import { IServiceModule } from '@/services';
import FileService from '@/services/fileSrv';
import { IpcClientEventSender } from '@/types/ipcClientEvent';
import { createLogger } from '@/utils/logger';
import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
import BrowserManager from './BrowserManager';
import { I18nManager } from './I18nManager';
import { IoCContainer } from './IoCContainer';
import MenuManager from './MenuManager';
import { ShortcutManager } from './ShortcutManager';
import { StaticFileServerManager } from './StaticFileServerManager';
import { StoreManager } from './StoreManager';
import TrayManager from './TrayManager';
import { UpdaterManager } from './UpdaterManager';
import { BrowserManager } from './browser/BrowserManager';
import { I18nManager } from './infrastructure/I18nManager';
import { IoCContainer } from './infrastructure/IoCContainer';
import { StaticFileServerManager } from './infrastructure/StaticFileServerManager';
import { StoreManager } from './infrastructure/StoreManager';
import { UpdaterManager } from './infrastructure/UpdaterManager';
import { MenuManager } from './ui/MenuManager';
import { ShortcutManager } from './ui/ShortcutManager';
import { TrayManager } from './ui/TrayManager';
const logger = createLogger('core:App');
@@ -1,173 +0,0 @@
import { globalShortcut } from 'electron';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
// Create logger
const logger = createLogger('core:ShortcutManager');
export class ShortcutManager {
private app: App;
private shortcuts: Map<string, () => void> = new Map();
private shortcutsConfig: Record<string, string> = {};
constructor(app: App) {
logger.debug('Initializing ShortcutManager');
this.app = app;
app.shortcutMethodMap.forEach((method, key) => {
this.shortcuts.set(key, method);
});
}
initialize() {
logger.info('Initializing global shortcuts');
// Load shortcuts configuration from storage
this.loadShortcutsConfig();
// Register configured shortcuts
this.registerConfiguredShortcuts();
}
/**
* Get shortcuts configuration
*/
getShortcutsConfig(): Record<string, string> {
return this.shortcutsConfig;
}
/**
* Update a single shortcut configuration
*/
updateShortcutConfig(id: string, accelerator: string): boolean {
try {
logger.debug(`Updating shortcut ${id} to ${accelerator}`);
// Update configuration
this.shortcutsConfig[id] = accelerator;
this.saveShortcutsConfig();
this.registerConfiguredShortcuts();
return true;
} catch (error) {
logger.error(`Error updating shortcut ${id}:`, error);
return false;
}
}
/**
* Register global shortcut
* @param accelerator Shortcut key combination
* @param callback Callback function
* @returns Whether registration was successful
*/
registerShortcut(accelerator: string, callback: () => void): boolean {
try {
// If already registered, unregister first
if (this.shortcuts.has(accelerator)) {
this.unregisterShortcut(accelerator);
}
// Register new shortcut
const success = globalShortcut.register(accelerator, callback);
if (success) {
this.shortcuts.set(accelerator, callback);
logger.debug(`Registered shortcut: ${accelerator}`);
} else {
logger.error(`Failed to register shortcut: ${accelerator}`);
}
return success;
} catch (error) {
logger.error(`Error registering shortcut: ${accelerator}`, error);
return false;
}
}
/**
* Unregister global shortcut
* @param accelerator Shortcut key combination
*/
unregisterShortcut(accelerator: string): void {
try {
globalShortcut.unregister(accelerator);
this.shortcuts.delete(accelerator);
logger.debug(`Unregistered shortcut: ${accelerator}`);
} catch (error) {
logger.error(`Error unregistering shortcut: ${accelerator}`, error);
}
}
/**
* Check if a shortcut is already registered
* @param accelerator Shortcut key combination
* @returns Whether it is registered
*/
isRegistered(accelerator: string): boolean {
return globalShortcut.isRegistered(accelerator);
}
/**
* Unregister all shortcuts
*/
unregisterAll(): void {
globalShortcut.unregisterAll();
logger.info('Unregistered all shortcuts');
}
/**
* Load shortcuts configuration from storage
*/
private loadShortcutsConfig() {
try {
// Try to get configuration from storage
const config = this.app.storeManager.get('shortcuts');
// If no configuration, use default configuration
if (!config || Object.keys(config).length === 0) {
logger.debug('No shortcuts config found, using defaults');
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
this.saveShortcutsConfig();
} else {
this.shortcutsConfig = config;
}
logger.debug('Loaded shortcuts config:', this.shortcutsConfig);
} catch (error) {
logger.error('Error loading shortcuts config:', error);
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
this.saveShortcutsConfig();
}
}
/**
* Save shortcuts configuration to storage
*/
private saveShortcutsConfig() {
try {
this.app.storeManager.set('shortcuts', this.shortcutsConfig);
logger.debug('Saved shortcuts config');
} catch (error) {
logger.error('Error saving shortcuts config:', error);
}
}
/**
* Register configured shortcuts
*/
private registerConfiguredShortcuts() {
// Unregister all shortcuts first
this.unregisterAll();
// Register each enabled shortcut
Object.entries(this.shortcutsConfig).forEach(([id, accelerator]) => {
logger.debug(`Registering shortcut '${id}' with ${accelerator}`);
const method = this.shortcuts.get(id);
if (accelerator && method) {
this.registerShortcut(accelerator, method);
}
});
}
}
@@ -6,13 +6,21 @@ import {
nativeTheme,
screen,
} from 'electron';
import os from 'node:os';
import { join } from 'node:path';
import { buildDir, preloadDir, resourcesDir } from '@/const/dir';
import { isDev, isWindows } from '@/const/env';
import {
BACKGROUND_DARK,
BACKGROUND_LIGHT,
SYMBOL_COLOR_DARK,
SYMBOL_COLOR_LIGHT,
THEME_CHANGE_DELAY,
TITLE_BAR_HEIGHT,
} from '@/const/theme';
import { createLogger } from '@/utils/logger';
import { preloadDir, resourcesDir } from '../const/dir';
import type { App } from './App';
import type { App } from '../App';
// Create logger
const logger = createLogger('core:Browser');
@@ -20,9 +28,6 @@ const logger = createLogger('core:Browser');
export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
devTools?: boolean;
height?: number;
/**
* URL
*/
identifier: string;
keepAlive?: boolean;
parentIdentifier?: string;
@@ -34,38 +39,18 @@ export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
export default class Browser {
private app: App;
/**
* Internal electron window
*/
private _browserWindow?: BrowserWindow;
private themeListenerSetup = false;
private stopInterceptHandler;
/**
* Identifier
*/
identifier: string;
/**
* Options at creation
*/
options: BrowserWindowOpts;
/**
* Key for storing window state in storeManager
*/
private readonly windowStateKey: string;
/**
* Method to expose window externally
*/
get browserWindow() {
return this.retrieveOrInitialize();
}
get webContents() {
if (this._browserWindow.isDestroyed()) return null;
return this._browserWindow.webContents;
}
@@ -86,6 +71,101 @@ export default class Browser {
this.retrieveOrInitialize();
}
/**
* Get platform-specific theme configuration for window creation
*/
private getPlatformThemeConfig(isDarkMode?: boolean): Record<string, any> {
const darkMode = isDarkMode ?? nativeTheme.shouldUseDarkColors;
if (isWindows) {
return this.getWindowsThemeConfig(darkMode);
}
return {};
}
/**
* Get Windows-specific theme configuration
*/
private getWindowsThemeConfig(isDarkMode: boolean) {
return {
backgroundColor: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
icon: isDev ? join(buildDir, 'icon-dev.ico') : undefined,
titleBarOverlay: {
color: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
height: TITLE_BAR_HEIGHT,
symbolColor: isDarkMode ? SYMBOL_COLOR_DARK : SYMBOL_COLOR_LIGHT,
},
titleBarStyle: 'hidden' as const,
};
}
private setupThemeListener(): void {
if (this.themeListenerSetup) return;
nativeTheme.on('updated', this.handleThemeChange);
this.themeListenerSetup = true;
}
private handleThemeChange = (): void => {
logger.debug(`[${this.identifier}] System theme changed, reapplying visual effects.`);
setTimeout(() => {
this.applyVisualEffects();
}, THEME_CHANGE_DELAY);
};
/**
* Handle application theme mode change (called from BrowserManager)
*/
handleAppThemeChange = (): void => {
logger.debug(`[${this.identifier}] App theme mode changed, reapplying visual effects.`);
setTimeout(() => {
this.applyVisualEffects();
}, THEME_CHANGE_DELAY);
};
private applyVisualEffects(): void {
if (!this._browserWindow || this._browserWindow.isDestroyed()) return;
logger.debug(`[${this.identifier}] Applying visual effects for platform`);
const isDarkMode = this.isDarkMode;
try {
if (isWindows) {
this.applyWindowsVisualEffects(isDarkMode);
}
logger.debug(
`[${this.identifier}] Visual effects applied successfully (dark mode: ${isDarkMode})`,
);
} catch (error) {
logger.error(`[${this.identifier}] Failed to apply visual effects:`, error);
}
}
private applyWindowsVisualEffects(isDarkMode: boolean): void {
const config = this.getWindowsThemeConfig(isDarkMode);
this._browserWindow.setBackgroundColor(config.backgroundColor);
this._browserWindow.setTitleBarOverlay(config.titleBarOverlay);
}
private cleanupThemeListener(): void {
if (this.themeListenerSetup) {
// Note: nativeTheme listeners are global, consider using a centralized theme manager
nativeTheme.off('updated', this.handleThemeChange);
// for multiple windows to avoid duplicate listeners
this.themeListenerSetup = false;
}
}
private get isDarkMode() {
const themeMode = this.app.storeManager.get('themeMode');
if (themeMode === 'auto') return nativeTheme.shouldUseDarkColors;
return themeMode === 'dark';
}
loadUrl = async (path: string) => {
const initUrl = this.app.nextServerUrl + path;
@@ -203,6 +283,7 @@ export default class Browser {
destroy() {
logger.debug(`Destroying window instance: ${this.identifier}`);
this.stopInterceptHandler?.();
this.cleanupThemeListener();
this._browserWindow = undefined;
}
@@ -228,45 +309,37 @@ export default class Browser {
`[${this.identifier}] Saved window state (only size used): ${JSON.stringify(savedState)}`,
);
const { isWindows11, isWindows } = this.getWindowsVersion();
const isDarkMode = nativeTheme.shouldUseDarkColors;
const browserWindow = new BrowserWindow({
...res,
...(isWindows
? {
titleBarStyle: 'hidden',
}
: {}),
...(isWindows11
? {
backgroundMaterial: isDarkMode ? 'mica' : 'acrylic',
vibrancy: 'under-window',
visualEffectState: 'active',
}
: {}),
autoHideMenuBar: true,
backgroundColor: '#00000000',
darkTheme: isDarkMode,
frame: false,
height: savedState?.height || height,
// Always create hidden first
show: false,
title,
vibrancy: 'sidebar',
visualEffectState: 'active',
webPreferences: {
// Context isolation environment
// https://www.electronjs.org/docs/tutorial/context-isolation
backgroundThrottling: false,
contextIsolation: true,
preload: join(preloadDir, 'index.js'),
},
width: savedState?.width || width,
...this.getPlatformThemeConfig(isDarkMode),
});
this._browserWindow = browserWindow;
logger.debug(`[${this.identifier}] BrowserWindow instance created.`);
if (isWindows11) this.applyVisualEffects();
// Initialize theme listener for this window to handle theme changes
this.setupThemeListener();
logger.debug(`[${this.identifier}] Theme listener setup and applying initial visual effects.`);
// Apply initial visual effects
this.applyVisualEffects();
logger.debug(`[${this.identifier}] Setting up nextInterceptor.`);
this.stopInterceptHandler = this.app.nextInterceptor({
@@ -320,8 +393,9 @@ export default class Browser {
} catch (error) {
logger.error(`[${this.identifier}] Failed to save window state on quit:`, error);
}
// Need to clean up intercept handler
// Need to clean up intercept handler and theme manager
this.stopInterceptHandler?.();
this.cleanupThemeListener();
return;
}
@@ -355,8 +429,9 @@ export default class Browser {
} catch (error) {
logger.error(`[${this.identifier}] Failed to save window state on close:`, error);
}
// Need to clean up intercept handler
// Need to clean up intercept handler and theme manager
this.stopInterceptHandler?.();
this.cleanupThemeListener();
}
});
@@ -387,16 +462,6 @@ export default class Browser {
this._browserWindow.webContents.send(channel, data);
};
applyVisualEffects() {
// Windows 11 can use this new API
if (this._browserWindow) {
logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
const isDarkMode = nativeTheme.shouldUseDarkColors;
this._browserWindow?.setBackgroundMaterial(isDarkMode ? 'mica' : 'acrylic');
this._browserWindow?.setVibrancy('under-window');
}
}
toggleVisible() {
logger.debug(`Toggling visibility for window: ${this.identifier}`);
if (this._browserWindow.isVisible() && this._browserWindow.isFocused()) {
@@ -407,35 +472,11 @@ export default class Browser {
}
}
getWindowsVersion() {
if (process.platform !== 'win32') {
return {
isWindows: false,
isWindows10: false,
isWindows11: false,
version: null,
};
}
// 获取操作系统版本(如 "10.0.22621"
const release = os.release();
const parts = release.split('.');
// 主版本和次版本
const majorVersion = parseInt(parts[0], 10);
const minorVersion = parseInt(parts[1], 10);
// 构建号是第三部分
const buildNumber = parseInt(parts[2], 10);
// Windows 11 的构建号从 22000 开始
const isWindows11 = majorVersion === 10 && minorVersion === 0 && buildNumber >= 22_000;
return {
buildNumber,
isWindows: true,
isWindows11,
version: release,
};
/**
* Manually reapply visual effects (useful for fixing lost effects after window state changes)
*/
reapplyVisualEffects(): void {
logger.debug(`[${this.identifier}] Manually reapplying visual effects via Browser.`);
this.applyVisualEffects();
}
}
@@ -3,15 +3,15 @@ import { WebContents } from 'electron';
import { createLogger } from '@/utils/logger';
import { AppBrowsersIdentifiers, appBrowsers } from '../appBrowsers';
import type { App } from './App';
import { AppBrowsersIdentifiers, appBrowsers } from '../../appBrowsers';
import type { App } from '../App';
import type { BrowserWindowOpts } from './Browser';
import Browser from './Browser';
// Create logger
const logger = createLogger('core:BrowserManager');
export default class BrowserManager {
export class BrowserManager {
app: App;
browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
@@ -194,4 +194,14 @@ export default class BrowserManager {
getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
return this.webContentsMap.get(webContents) || null;
}
/**
* Handle application theme mode changes and reapply visual effects to all windows
*/
handleAppThemeChange(): void {
logger.debug('Handling app theme change for all browser windows');
this.browsers.forEach((browser) => {
browser.handleAppThemeChange();
});
}
}
@@ -5,7 +5,7 @@ import { LOCAL_STORAGE_URL_PREFIX } from '@/const/dir';
import FileService from '@/services/fileSrv';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import type { App } from '../App';
const logger = createLogger('core:StaticFileServerManager');
@@ -54,9 +54,12 @@ export class StaticFileServerManager {
try {
// 使用 get-port-please 获取可用端口
this.serverPort = await getPort({
port: 33250, // 首选端口
ports: [33251, 33252, 33253, 33254, 33255], // 备用端口
// 备用端口
host: '127.0.0.1',
port: 33_250,
// 首选端口
ports: [33_251, 33_252, 33_253, 33_254, 33_255],
});
logger.debug(`Found available port: ${this.serverPort}`);
@@ -64,7 +67,7 @@ export class StaticFileServerManager {
return new Promise((resolve, reject) => {
const server = createServer(async (req, res) => {
// 设置请求超时
req.setTimeout(30000, () => {
req.setTimeout(30_000, () => {
logger.warn('Request timeout, closing connection');
if (!res.destroyed && !res.headersSent) {
res.writeHead(408, { 'Content-Type': 'text/plain' });
@@ -155,10 +158,13 @@ export class StaticFileServerManager {
// 设置响应头
res.writeHead(200, {
'Content-Type': fileResult.mimeType,
'Cache-Control': 'public, max-age=31536000', // 缓存一年
'Access-Control-Allow-Origin': 'http://localhost:*', // 允许 localhost 的任意端口
// 缓存一年
'Access-Control-Allow-Origin': 'http://localhost:*',
'Cache-Control': 'public, max-age=31536000',
// 允许 localhost 的任意端口
'Content-Length': Buffer.byteLength(fileResult.content),
'Content-Type': fileResult.mimeType,
});
// 发送文件内容
@@ -5,7 +5,7 @@ import { ElectronMainStore, StoreKey } from '@/types/store';
import { makeSureDirExist } from '@/utils/file-system';
import { createLogger } from '@/utils/logger';
import { App } from './App';
import { App } from '../App';
// Create logger
const logger = createLogger('core:StoreManager');
@@ -5,7 +5,7 @@ import { isDev } from '@/const/env';
import { UPDATE_CHANNEL as channel, updaterConfig } from '@/modules/updater/configs';
import { createLogger } from '@/utils/logger';
import type { App as AppCore } from './App';
import type { App as AppCore } from '../App';
// Create logger
const logger = createLogger('core:UpdaterManager');
@@ -3,12 +3,12 @@ import { Menu } from 'electron';
import { IMenuPlatform, MenuOptions, createMenuImpl } from '@/menus';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import type { App } from '../App';
// Create logger
const logger = createLogger('core:MenuManager');
export default class MenuManager {
export class MenuManager {
app: App;
private platformImpl: IMenuPlatform;
@@ -0,0 +1,300 @@
import { globalShortcut } from 'electron';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import { createLogger } from '@/utils/logger';
import type { App } from '../App';
// Create logger
const logger = createLogger('core:ShortcutManager');
export interface ShortcutUpdateResult {
errorType?:
| 'INVALID_ID'
| 'INVALID_FORMAT'
| 'NO_MODIFIER'
| 'CONFLICT'
| 'SYSTEM_OCCUPIED'
| 'UNKNOWN';
success: boolean;
}
export class ShortcutManager {
private app: App;
private shortcuts: Map<string, () => void> = new Map();
private shortcutsConfig: Record<string, string> = {};
constructor(app: App) {
logger.debug('Initializing ShortcutManager');
this.app = app;
app.shortcutMethodMap.forEach((method, key) => {
this.shortcuts.set(key, method);
});
}
/**
* Convert react-hotkey format to Electron accelerator format
* @param accelerator The accelerator string from frontend
* @returns Converted accelerator string for Electron
*/
private convertAcceleratorFormat(accelerator: string): string {
return accelerator
.split('+')
.map((key) => {
const trimmedKey = key.trim().toLowerCase();
// Convert react-hotkey 'mod' to Electron 'CommandOrControl'
if (trimmedKey === 'mod') {
return 'CommandOrControl';
}
// Keep other keys as is, but preserve proper casing
return key.trim().length === 1 ? key.trim().toUpperCase() : key.trim();
})
.join('+');
}
initialize() {
logger.info('Initializing global shortcuts');
// Load shortcuts configuration from storage
this.loadShortcutsConfig();
// Register configured shortcuts
this.registerConfiguredShortcuts();
}
/**
* Get shortcuts configuration
*/
getShortcutsConfig(): Record<string, string> {
return this.shortcutsConfig;
}
/**
* Update a single shortcut configuration
*/
updateShortcutConfig(id: string, accelerator: string): ShortcutUpdateResult {
try {
logger.debug(`Updating shortcut ${id} to ${accelerator}`);
// 1. 检查 ID 是否有效
if (!DEFAULT_SHORTCUTS_CONFIG[id]) {
logger.error(`Invalid shortcut ID: ${id}`);
return { errorType: 'INVALID_ID', success: false };
}
// 2. 基本格式校验
if (!accelerator || typeof accelerator !== 'string' || accelerator.trim() === '') {
logger.error(`Invalid accelerator format: ${accelerator}`);
return { errorType: 'INVALID_FORMAT', success: false };
}
// 转换前端格式到 Electron 格式
const convertedAccelerator = this.convertAcceleratorFormat(accelerator.trim());
const cleanAccelerator = convertedAccelerator.toLowerCase();
logger.debug(`Converted accelerator from ${accelerator} to ${convertedAccelerator}`);
// 3. 检查是否包含 + 号(修饰键格式)
if (!cleanAccelerator.includes('+')) {
logger.error(
`Invalid accelerator format: ${cleanAccelerator}. Must contain modifier keys like 'CommandOrControl+E'`,
);
return { errorType: 'INVALID_FORMAT', success: false };
}
// 4. 检查是否有基本的修饰键
const hasModifier = ['CommandOrControl', 'Command', 'Ctrl', 'Alt', 'Shift'].some((modifier) =>
cleanAccelerator.includes(modifier.toLowerCase()),
);
if (!hasModifier) {
logger.error(`Invalid accelerator format: ${cleanAccelerator}. Must contain modifier keys`);
return { errorType: 'NO_MODIFIER', success: false };
}
// 5. 检查冲突
for (const [existingId, existingAccelerator] of Object.entries(this.shortcutsConfig)) {
if (
existingId !== id &&
typeof existingAccelerator === 'string' &&
existingAccelerator.toLowerCase() === cleanAccelerator
) {
logger.error(`Shortcut conflict: ${cleanAccelerator} already used by ${existingId}`);
return { errorType: 'CONFLICT', success: false };
}
}
// 6. 尝试注册测试(检查是否被系统占用)
const testSuccess = globalShortcut.register(convertedAccelerator, () => {});
if (!testSuccess) {
logger.error(
`Shortcut ${convertedAccelerator} is already registered by system or other app`,
);
return { errorType: 'SYSTEM_OCCUPIED', success: false };
} else {
// 测试成功,立即取消注册
globalShortcut.unregister(convertedAccelerator);
}
// 7. 更新配置
this.shortcutsConfig[id] = convertedAccelerator;
this.saveShortcutsConfig();
this.registerConfiguredShortcuts();
return { success: true };
} catch (error) {
logger.error(`Error updating shortcut ${id}:`, error);
return { errorType: 'UNKNOWN', success: false };
}
}
/**
* Register global shortcut
* @param accelerator Shortcut key combination
* @param callback Callback function
* @returns Whether registration was successful
*/
registerShortcut(accelerator: string, callback: () => void): boolean {
try {
// If already registered, unregister first
if (this.shortcuts.has(accelerator)) {
this.unregisterShortcut(accelerator);
}
// Register new shortcut
const success = globalShortcut.register(accelerator, callback);
if (success) {
this.shortcuts.set(accelerator, callback);
logger.debug(`Registered shortcut: ${accelerator}`);
} else {
logger.error(`Failed to register shortcut: ${accelerator}`);
}
return success;
} catch (error) {
logger.error(`Error registering shortcut: ${accelerator}`, error);
return false;
}
}
/**
* Unregister global shortcut
* @param accelerator Shortcut key combination
*/
unregisterShortcut(accelerator: string): void {
try {
globalShortcut.unregister(accelerator);
this.shortcuts.delete(accelerator);
logger.debug(`Unregistered shortcut: ${accelerator}`);
} catch (error) {
logger.error(`Error unregistering shortcut: ${accelerator}`, error);
}
}
/**
* Check if a shortcut is already registered
* @param accelerator Shortcut key combination
* @returns Whether it is registered
*/
isRegistered(accelerator: string): boolean {
return globalShortcut.isRegistered(accelerator);
}
/**
* Unregister all shortcuts
*/
unregisterAll(): void {
globalShortcut.unregisterAll();
logger.info('Unregistered all shortcuts');
}
/**
* Load shortcuts configuration from storage
*/
private loadShortcutsConfig() {
try {
// Try to get configuration from storage
const config = this.app.storeManager.get('shortcuts');
// If no configuration, use default configuration
if (!config || Object.keys(config).length === 0) {
logger.debug('No shortcuts config found, using defaults');
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
this.saveShortcutsConfig();
} else {
// Filter out invalid shortcuts that are not in DEFAULT_SHORTCUTS_CONFIG
const filteredConfig: Record<string, string> = {};
let hasInvalidKeys = false;
Object.entries(config).forEach(([id, accelerator]) => {
if (DEFAULT_SHORTCUTS_CONFIG[id]) {
filteredConfig[id] = accelerator;
} else {
hasInvalidKeys = true;
logger.debug(`Filtering out invalid shortcut ID: ${id}`);
}
});
// Ensure all default shortcuts are present
Object.entries(DEFAULT_SHORTCUTS_CONFIG).forEach(([id, defaultAccelerator]) => {
if (!(id in filteredConfig)) {
filteredConfig[id] = defaultAccelerator;
logger.debug(`Adding missing default shortcut: ${id} = ${defaultAccelerator}`);
}
});
this.shortcutsConfig = filteredConfig;
// Save the filtered configuration back to storage if we removed invalid keys
if (hasInvalidKeys) {
logger.debug('Saving filtered shortcuts config to remove invalid keys');
this.saveShortcutsConfig();
}
}
logger.debug('Loaded shortcuts config:', this.shortcutsConfig);
} catch (error) {
logger.error('Error loading shortcuts config:', error);
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
this.saveShortcutsConfig();
}
}
/**
* Save shortcuts configuration to storage
*/
private saveShortcutsConfig() {
try {
this.app.storeManager.set('shortcuts', this.shortcutsConfig);
logger.debug('Saved shortcuts config');
} catch (error) {
logger.error('Error saving shortcuts config:', error);
}
}
/**
* Register configured shortcuts
*/
private registerConfiguredShortcuts() {
// Unregister all shortcuts first
this.unregisterAll();
// Register each enabled shortcut
Object.entries(this.shortcutsConfig).forEach(([id, accelerator]) => {
logger.debug(`Registering shortcut '${id}' with ${accelerator}`);
// 只注册在 DEFAULT_SHORTCUTS_CONFIG 中存在的快捷键
if (!DEFAULT_SHORTCUTS_CONFIG[id]) {
logger.debug(`Skipping shortcut '${id}' - not found in DEFAULT_SHORTCUTS_CONFIG`);
return;
}
const method = this.shortcuts.get(id);
if (accelerator && method) {
this.registerShortcut(accelerator, method);
}
});
}
}
@@ -12,157 +12,157 @@ import { join } from 'node:path';
import { resourcesDir } from '@/const/dir';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import type { App } from '../App';
// 创建日志记录器
// Create logger
const logger = createLogger('core:Tray');
export interface TrayOptions {
/**
*
* Tray icon path (relative to resource directory)
*/
iconPath: string;
/**
*
* Tray identifier
*/
identifier: string;
/**
*
* Tray tooltip text
*/
tooltip?: string;
}
export default class Tray {
export class Tray {
private app: App;
/**
* Electron
* Internal Electron tray
*/
private _tray?: ElectronTray;
/**
*
* Identifier
*/
identifier: string;
/**
*
* Options when created
*/
options: TrayOptions;
/**
*
* Get tray instance
*/
get tray() {
return this.retrieveOrInitialize();
}
/**
*
* @param options
* @param application
* Construct tray object
* @param options Tray options
* @param application App instance
*/
constructor(options: TrayOptions, application: App) {
logger.debug(`创建托盘实例: ${options.identifier}`);
logger.debug(`托盘选项: ${JSON.stringify(options)}`);
logger.debug(`Creating tray instance: ${options.identifier}`);
logger.debug(`Tray options: ${JSON.stringify(options)}`);
this.app = application;
this.identifier = options.identifier;
this.options = options;
// 初始化
// Initialize
this.retrieveOrInitialize();
}
/**
*
* Initialize tray
*/
retrieveOrInitialize() {
// 如果托盘已存在且未被销毁,则返回
// If tray already exists and is not destroyed, return it
if (this._tray) {
logger.debug(`[${this.identifier}] 返回现有托盘实例`);
logger.debug(`[${this.identifier}] Returning existing tray instance`);
return this._tray;
}
const { iconPath, tooltip } = this.options;
// 加载托盘图标
logger.info(`创建新的托盘实例: ${this.identifier}`);
// Load tray icon
logger.info(`Creating new tray instance: ${this.identifier}`);
const iconFile = join(resourcesDir, iconPath);
logger.debug(`[${this.identifier}] 加载图标: ${iconFile}`);
logger.debug(`[${this.identifier}] Loading icon: ${iconFile}`);
try {
const icon = nativeImage.createFromPath(iconFile);
this._tray = new ElectronTray(icon);
// 设置工具提示
// Set tooltip
if (tooltip) {
logger.debug(`[${this.identifier}] 设置提示文本: ${tooltip}`);
logger.debug(`[${this.identifier}] Setting tooltip: ${tooltip}`);
this._tray.setToolTip(tooltip);
}
// 设置默认上下文菜单
// Set default context menu
this.setContextMenu();
// 设置点击事件
// Set click event
this._tray.on('click', () => {
logger.debug(`[${this.identifier}] 托盘被点击`);
logger.debug(`[${this.identifier}] Tray clicked`);
this.onClick();
});
logger.debug(`[${this.identifier}] 托盘实例创建完成`);
logger.debug(`[${this.identifier}] Tray instance created successfully`);
return this._tray;
} catch (error) {
logger.error(`[${this.identifier}] 创建托盘失败:`, error);
logger.error(`[${this.identifier}] Failed to create tray:`, error);
throw error;
}
}
/**
*
* @param template 使
* Set tray context menu
* @param template Menu template, if not provided default template will be used
*/
setContextMenu(template?: MenuItemConstructorOptions[]) {
logger.debug(`[${this.identifier}] 设置托盘上下文菜单`);
logger.debug(`[${this.identifier}] Setting tray context menu`);
// 如果未提供模板,使用默认菜单
// If no template provided, use default menu
const defaultTemplate: MenuItemConstructorOptions[] = template || [
{
click: () => {
logger.debug(`[${this.identifier}] 菜单项 "显示主窗口" 被点击`);
logger.debug(`[${this.identifier}] Menu item "Show Main Window" clicked`);
this.app.browserManager.showMainWindow();
},
label: '显示主窗口',
label: 'Show Main Window',
},
{ type: 'separator' },
{
click: () => {
logger.debug(`[${this.identifier}] 菜单项 "退出" 被点击`);
logger.debug(`[${this.identifier}] Menu item "Quit" clicked`);
app.quit();
},
label: '退出',
label: 'Quit',
},
];
const contextMenu = Menu.buildFromTemplate(defaultTemplate);
this._tray?.setContextMenu(contextMenu);
logger.debug(`[${this.identifier}] 托盘上下文菜单已设置`);
logger.debug(`[${this.identifier}] Tray context menu has been set`);
}
/**
*
* Handle tray click event
*/
onClick() {
logger.debug(`[${this.identifier}] 处理托盘点击事件`);
logger.debug(`[${this.identifier}] Handling tray click event`);
const mainWindow = this.app.browserManager.getMainWindow();
if (mainWindow) {
if (mainWindow.browserWindow.isVisible() && mainWindow.browserWindow.isFocused()) {
logger.debug(`[${this.identifier}] 主窗口已可见且聚焦,现在隐藏它`);
logger.debug(`[${this.identifier}] Main window is visible and focused, hiding it now`);
mainWindow.hide();
} else {
logger.debug(`[${this.identifier}] 显示并聚焦主窗口`);
logger.debug(`[${this.identifier}] Showing and focusing main window`);
mainWindow.show();
mainWindow.browserWindow.focus();
}
@@ -170,59 +170,61 @@ export default class Tray {
}
/**
*
* @param iconPath
* Update tray icon
* @param iconPath New icon path (relative to resource directory)
*/
updateIcon(iconPath: string) {
logger.debug(`[${this.identifier}] 更新图标: ${iconPath}`);
logger.debug(`[${this.identifier}] Updating icon: ${iconPath}`);
try {
const iconFile = join(resourcesDir, iconPath);
const icon = nativeImage.createFromPath(iconFile);
this._tray?.setImage(icon);
this.options.iconPath = iconPath;
logger.debug(`[${this.identifier}] 图标已更新`);
logger.debug(`[${this.identifier}] Icon updated successfully`);
} catch (error) {
logger.error(`[${this.identifier}] 更新图标失败:`, error);
logger.error(`[${this.identifier}] Failed to update icon:`, error);
}
}
/**
*
* @param tooltip
* Update tooltip text
* @param tooltip New tooltip text
*/
updateTooltip(tooltip: string) {
logger.debug(`[${this.identifier}] 更新提示文本: ${tooltip}`);
logger.debug(`[${this.identifier}] Updating tooltip: ${tooltip}`);
this._tray?.setToolTip(tooltip);
this.options.tooltip = tooltip;
}
/**
* Windows
* @param options
* Display balloon notification (only supported on Windows)
* @param options Balloon options
*/
displayBalloon(options: DisplayBalloonOptions) {
if (process.platform === 'win32' && this._tray) {
logger.debug(`[${this.identifier}] 显示气泡通知: ${JSON.stringify(options)}`);
logger.debug(
`[${this.identifier}] Displaying balloon notification: ${JSON.stringify(options)}`,
);
this._tray.displayBalloon(options);
} else {
logger.debug(`[${this.identifier}] 气泡通知仅在 Windows 上支持`);
logger.debug(`[${this.identifier}] Balloon notification is only supported on Windows`);
}
}
/**
* 广
* Broadcast event
*/
broadcast = <T extends MainBroadcastEventKey>(channel: T, data?: MainBroadcastParams<T>) => {
logger.debug(`向托盘 ${this.identifier} 广播, 频道: ${channel}`);
// 可以通过 App 实例的 browserManager 将消息转发到主窗口
logger.debug(`Broadcasting to tray ${this.identifier}, channel: ${channel}`);
// Can forward message to main window through App instance's browserManager
this.app.browserManager.getMainWindow()?.broadcast(channel, data);
};
/**
*
* Destroy tray instance
*/
destroy() {
logger.debug(`销毁托盘实例: ${this.identifier}`);
logger.debug(`Destroying tray instance: ${this.identifier}`);
if (this._tray) {
this._tray.destroy();
this._tray = undefined;
@@ -5,8 +5,8 @@ import { name } from '@/../../package.json';
import { isMac } from '@/const/env';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
import Tray, { TrayOptions } from './Tray';
import type { App } from '../App';
import { Tray, TrayOptions } from './Tray';
// 创建日志记录器
const logger = createLogger('core:TrayManager');
@@ -16,7 +16,7 @@ const logger = createLogger('core:TrayManager');
*/
export type TrayIdentifiers = 'main';
export default class TrayManager {
export class TrayManager {
app: App;
/**
@@ -61,8 +61,8 @@ export default class TrayManager {
? 'tray-dark.png'
: 'tray-light.png'
: 'tray.png',
identifier: 'main', // 使用应用图标,需要确保资源目录中有此文件
tooltip: name, // 可以使用 app.getName() 或本地化字符串
identifier: 'main', // Use app icon, ensure this file exists in resources directory
tooltip: name, // Can use app.getName() or localized string
});
}
@@ -0,0 +1,539 @@
import { globalShortcut } from 'electron';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import type { App } from '../../App';
import { ShortcutManager } from '../ShortcutManager';
// Mock electron
vi.mock('electron', () => ({
globalShortcut: {
register: vi.fn(),
unregister: vi.fn(),
unregisterAll: vi.fn(),
isRegistered: vi.fn(),
},
}));
// Mock Logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock DEFAULT_SHORTCUTS_CONFIG
vi.mock('@/shortcuts', () => ({
DEFAULT_SHORTCUTS_CONFIG: {
showApp: 'Control+E',
openSettings: 'CommandOrControl+,',
},
}));
describe('ShortcutManager', () => {
let shortcutManager: ShortcutManager;
let mockApp: App;
let mockStoreManager: any;
let mockShortcutMethodMap: Map<string, () => void>;
beforeEach(() => {
vi.clearAllMocks();
// Reset all mocks to their default behavior
vi.mocked(globalShortcut.register).mockReturnValue(true);
vi.mocked(globalShortcut.unregister).mockReturnValue(undefined);
vi.mocked(globalShortcut.unregisterAll).mockReturnValue(undefined);
vi.mocked(globalShortcut.isRegistered).mockReturnValue(false);
// Mock store manager
mockStoreManager = {
get: vi.fn(),
set: vi.fn(),
};
// Mock shortcut method map
mockShortcutMethodMap = new Map();
const showAppMethod = vi.fn();
const openSettingsMethod = vi.fn();
mockShortcutMethodMap.set('showApp', showAppMethod);
mockShortcutMethodMap.set('openSettings', openSettingsMethod);
// Mock App
mockApp = {
storeManager: mockStoreManager,
shortcutMethodMap: mockShortcutMethodMap,
} as unknown as App;
shortcutManager = new ShortcutManager(mockApp);
});
describe('constructor', () => {
it('should initialize shortcut manager with app', () => {
expect(shortcutManager).toBeDefined();
expect(shortcutManager['app']).toBe(mockApp);
});
it('should populate shortcuts map from app shortcut method map', () => {
expect(shortcutManager['shortcuts'].size).toBe(2);
expect(shortcutManager['shortcuts'].has('showApp')).toBe(true);
expect(shortcutManager['shortcuts'].has('openSettings')).toBe(true);
});
});
describe('convertAcceleratorFormat', () => {
it('should convert mod to CommandOrControl', () => {
const result = shortcutManager['convertAcceleratorFormat']('mod+e');
expect(result).toBe('CommandOrControl+E');
});
it('should preserve other keys as is except single characters', () => {
const result = shortcutManager['convertAcceleratorFormat']('ctrl+alt+f12');
expect(result).toBe('ctrl+alt+f12');
});
it('should handle single character keys with uppercase', () => {
const result = shortcutManager['convertAcceleratorFormat']('ctrl + a');
expect(result).toBe('ctrl+A');
});
it('should handle complex combinations', () => {
const result = shortcutManager['convertAcceleratorFormat']('mod+shift+delete');
expect(result).toBe('CommandOrControl+shift+delete');
});
});
describe('initialize', () => {
it('should load shortcuts config and register shortcuts', () => {
// Mock store to return empty config (will use defaults)
mockStoreManager.get.mockReturnValue({});
shortcutManager.initialize();
expect(mockStoreManager.get).toHaveBeenCalledWith('shortcuts');
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
expect(globalShortcut.register).toHaveBeenCalledWith('Control+E', expect.any(Function));
expect(globalShortcut.register).toHaveBeenCalledWith(
'CommandOrControl+,',
expect.any(Function),
);
});
it('should handle stored config with filtering', () => {
const storedConfig = {
showApp: 'Alt+E',
openSettings: 'Ctrl+Shift+P',
invalidKey: 'Ctrl+I', // Should be filtered out
};
mockStoreManager.get.mockReturnValue(storedConfig);
shortcutManager.initialize();
const config = shortcutManager.getShortcutsConfig();
expect(config.showApp).toBe('Alt+E');
expect(config.openSettings).toBe('Ctrl+Shift+P');
expect(config.invalidKey).toBeUndefined();
});
});
describe('getShortcutsConfig', () => {
it('should return current shortcuts configuration', () => {
mockStoreManager.get.mockReturnValue({});
shortcutManager.initialize();
const config = shortcutManager.getShortcutsConfig();
expect(config).toEqual(DEFAULT_SHORTCUTS_CONFIG);
});
});
describe('updateShortcutConfig', () => {
beforeEach(() => {
mockStoreManager.get.mockReturnValue({});
shortcutManager.initialize();
});
it('should successfully update valid shortcut', () => {
const result = shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
expect(result.success).toBe(true);
expect(result.errorType).toBeUndefined();
expect(mockStoreManager.set).toHaveBeenCalledWith(
'shortcuts',
expect.objectContaining({
showApp: 'Alt+E',
}),
);
});
it('should reject invalid shortcut ID', () => {
const result = shortcutManager.updateShortcutConfig('invalidId', 'Alt+E');
expect(result.success).toBe(false);
expect(result.errorType).toBe('INVALID_ID');
});
it('should reject empty accelerator', () => {
const result = shortcutManager.updateShortcutConfig('showApp', '');
expect(result.success).toBe(false);
expect(result.errorType).toBe('INVALID_FORMAT');
});
it('should reject accelerator without modifier keys', () => {
const result = shortcutManager.updateShortcutConfig('showApp', 'E');
expect(result.success).toBe(false);
expect(result.errorType).toBe('INVALID_FORMAT');
});
it('should reject accelerator without proper modifiers', () => {
const result = shortcutManager.updateShortcutConfig('showApp', 'F1+E');
expect(result.success).toBe(false);
expect(result.errorType).toBe('NO_MODIFIER');
});
it('should detect conflicts with existing shortcuts', () => {
// First set a shortcut
shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
// Try to set the same accelerator for another shortcut
const result = shortcutManager.updateShortcutConfig('openSettings', 'Alt+E');
expect(result.success).toBe(false);
expect(result.errorType).toBe('CONFLICT');
});
it('should detect system occupied shortcuts', () => {
vi.mocked(globalShortcut.register).mockReturnValue(false);
const result = shortcutManager.updateShortcutConfig('showApp', 'Ctrl+Alt+T');
expect(result.success).toBe(false);
expect(result.errorType).toBe('SYSTEM_OCCUPIED');
});
it('should handle registration test cleanup', () => {
vi.mocked(globalShortcut.register).mockReturnValue(true);
shortcutManager.updateShortcutConfig('showApp', 'Ctrl+Alt+T');
// Should unregister the test registration
expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+Alt+T');
});
it('should handle conversion from react-hotkey format', () => {
const result = shortcutManager.updateShortcutConfig('showApp', 'mod+shift+e');
expect(result.success).toBe(true);
const config = shortcutManager.getShortcutsConfig();
expect(config.showApp).toBe('CommandOrControl+shift+E');
});
it('should handle errors gracefully', () => {
// Mock globalShortcut.register to throw an error during testing
vi.mocked(globalShortcut.register).mockImplementation(() => {
throw new Error('Register error');
});
const result = shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
expect(result.success).toBe(false);
expect(result.errorType).toBe('UNKNOWN');
});
});
describe('registerShortcut', () => {
it('should register new shortcut successfully', () => {
const callback = vi.fn();
vi.mocked(globalShortcut.register).mockReturnValue(true);
const result = shortcutManager.registerShortcut('Ctrl+T', callback);
expect(result).toBe(true);
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+T', callback);
expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(true);
});
it('should unregister existing shortcut before registering new one', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
// First registration
shortcutManager['shortcuts'].set('Ctrl+T', callback1);
vi.mocked(globalShortcut.register).mockReturnValue(true);
shortcutManager.registerShortcut('Ctrl+T', callback2);
expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+T');
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+T', callback2);
});
it('should handle registration failure', () => {
const callback = vi.fn();
vi.mocked(globalShortcut.register).mockReturnValue(false);
const result = shortcutManager.registerShortcut('Ctrl+T', callback);
expect(result).toBe(false);
expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(false);
});
it('should handle registration errors', () => {
const callback = vi.fn();
vi.mocked(globalShortcut.register).mockImplementation(() => {
throw new Error('Registration error');
});
const result = shortcutManager.registerShortcut('Ctrl+T', callback);
expect(result).toBe(false);
});
});
describe('unregisterShortcut', () => {
it('should unregister shortcut successfully', () => {
const callback = vi.fn();
shortcutManager['shortcuts'].set('Ctrl+T', callback);
shortcutManager.unregisterShortcut('Ctrl+T');
expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+T');
expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(false);
});
it('should handle unregistration errors', () => {
vi.mocked(globalShortcut.unregister).mockImplementation(() => {
throw new Error('Unregister error');
});
// Should not throw
expect(() => shortcutManager.unregisterShortcut('Ctrl+T')).not.toThrow();
});
});
describe('isRegistered', () => {
it('should check if shortcut is registered', () => {
vi.mocked(globalShortcut.isRegistered).mockReturnValue(true);
const result = shortcutManager.isRegistered('Ctrl+T');
expect(result).toBe(true);
expect(globalShortcut.isRegistered).toHaveBeenCalledWith('Ctrl+T');
});
});
describe('unregisterAll', () => {
it('should unregister all shortcuts', () => {
shortcutManager.unregisterAll();
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
});
});
describe('loadShortcutsConfig', () => {
it('should use defaults when no config exists', () => {
mockStoreManager.get.mockReturnValue(null);
shortcutManager['loadShortcutsConfig']();
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
});
it('should use defaults when config is empty', () => {
mockStoreManager.get.mockReturnValue({});
shortcutManager['loadShortcutsConfig']();
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
});
it('should filter invalid keys from stored config', () => {
const storedConfig = {
showApp: 'Alt+E',
openSettings: 'Ctrl+P',
invalidKey1: 'Ctrl+I',
invalidKey2: 'Ctrl+J',
};
mockStoreManager.get.mockReturnValue(storedConfig);
shortcutManager['loadShortcutsConfig']();
const config = shortcutManager['shortcutsConfig'];
expect(config.showApp).toBe('Alt+E');
expect(config.openSettings).toBe('Ctrl+P');
expect(config.invalidKey1).toBeUndefined();
expect(config.invalidKey2).toBeUndefined();
// Should save filtered config
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', config);
});
it('should add missing default shortcuts', () => {
const incompleteConfig = {
showApp: 'Alt+E',
// Missing openSettings
};
mockStoreManager.get.mockReturnValue(incompleteConfig);
shortcutManager['loadShortcutsConfig']();
const config = shortcutManager['shortcutsConfig'];
expect(config.showApp).toBe('Alt+E');
expect(config.openSettings).toBe('CommandOrControl+,'); // Default value
});
it('should not save config if no invalid keys were found', () => {
const validConfig = {
showApp: 'Alt+E',
openSettings: 'Ctrl+P',
};
mockStoreManager.get.mockReturnValue(validConfig);
shortcutManager['loadShortcutsConfig']();
// Should not call set since no changes were made
expect(mockStoreManager.set).not.toHaveBeenCalled();
});
it('should handle store errors gracefully', () => {
mockStoreManager.get.mockImplementation(() => {
throw new Error('Store error');
});
shortcutManager['loadShortcutsConfig']();
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
});
});
describe('saveShortcutsConfig', () => {
it('should save shortcuts config to store', () => {
shortcutManager['shortcutsConfig'] = { showApp: 'Alt+E', openSettings: 'Ctrl+P' };
shortcutManager['saveShortcutsConfig']();
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', {
showApp: 'Alt+E',
openSettings: 'Ctrl+P',
});
});
it('should handle save errors gracefully', () => {
mockStoreManager.set.mockImplementation(() => {
throw new Error('Save error');
});
// Should not throw
expect(() => shortcutManager['saveShortcutsConfig']()).not.toThrow();
});
});
describe('registerConfiguredShortcuts', () => {
beforeEach(() => {
shortcutManager['shortcutsConfig'] = {
showApp: 'Alt+E',
openSettings: 'Ctrl+P',
};
});
it('should register all configured shortcuts', () => {
vi.mocked(globalShortcut.register).mockReturnValue(true);
shortcutManager['registerConfiguredShortcuts']();
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
});
it('should skip shortcuts not in DEFAULT_SHORTCUTS_CONFIG', () => {
shortcutManager['shortcutsConfig'] = {
showApp: 'Alt+E',
invalidKey: 'Ctrl+I',
};
shortcutManager['registerConfiguredShortcuts']();
expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
expect(globalShortcut.register).not.toHaveBeenCalledWith('Ctrl+I', expect.any(Function));
});
it('should skip shortcuts with empty accelerator', () => {
shortcutManager['shortcutsConfig'] = {
showApp: '',
openSettings: 'Ctrl+P',
};
shortcutManager['registerConfiguredShortcuts']();
expect(globalShortcut.register).not.toHaveBeenCalledWith('', expect.any(Function));
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
});
it('should skip shortcuts without corresponding methods', () => {
// Remove method from map
mockShortcutMethodMap.delete('openSettings');
shortcutManager = new ShortcutManager(mockApp);
shortcutManager['shortcutsConfig'] = {
showApp: 'Alt+E',
openSettings: 'Ctrl+P',
};
shortcutManager['registerConfiguredShortcuts']();
expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
expect(globalShortcut.register).not.toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
});
});
describe('integration tests', () => {
it('should complete full initialization flow', () => {
const storedConfig = {
showApp: 'Alt+E',
openSettings: 'Ctrl+Shift+P',
invalidKey: 'Ctrl+I',
};
mockStoreManager.get.mockReturnValue(storedConfig);
vi.mocked(globalShortcut.register).mockReturnValue(true);
shortcutManager.initialize();
// Should filter config and register valid shortcuts
const config = shortcutManager.getShortcutsConfig();
expect(config.showApp).toBe('Alt+E');
expect(config.openSettings).toBe('Ctrl+Shift+P');
expect(config.invalidKey).toBeUndefined();
expect(globalShortcut.register).toHaveBeenCalledTimes(2);
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', config);
});
it('should handle complete update workflow', () => {
mockStoreManager.get.mockReturnValue({});
shortcutManager.initialize();
// Update a shortcut
const result = shortcutManager.updateShortcutConfig('showApp', 'mod+alt+e');
expect(result.success).toBe(true);
// Should convert format and register
const config = shortcutManager.getShortcutsConfig();
expect(config.showApp).toBe('CommandOrControl+alt+E');
// Should have saved and re-registered shortcuts
expect(mockStoreManager.set).toHaveBeenCalled();
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
expect(globalShortcut.register).toHaveBeenCalledWith(
'CommandOrControl+alt+E',
expect.any(Function),
);
});
});
});
+4 -2
View File
@@ -2,10 +2,11 @@
* 快捷键操作类型枚举
*/
export const ShortcutActionEnum = {
openSettings: 'openSettings',
/**
* 显示/隐藏主窗口
*/
toggleMainWindow: 'toggleMainWindow',
showApp: 'showApp',
} as const;
export type ShortcutActionType = (typeof ShortcutActionEnum)[keyof typeof ShortcutActionEnum];
@@ -14,5 +15,6 @@ export type ShortcutActionType = (typeof ShortcutActionEnum)[keyof typeof Shortc
* 默认快捷键配置
*/
export const DEFAULT_SHORTCUTS_CONFIG: Record<ShortcutActionType, string> = {
[ShortcutActionEnum.toggleMainWindow]: 'CommandOrControl+E',
[ShortcutActionEnum.showApp]: 'Control+E',
[ShortcutActionEnum.openSettings]: 'CommandOrControl+,',
};
+1
View File
@@ -11,6 +11,7 @@ export interface ElectronMainStore {
networkProxy: NetworkProxySettings;
shortcuts: Record<string, string>;
storagePath: string;
themeMode: 'dark' | 'light' | 'auto';
}
export type StoreKey = keyof ElectronMainStore;
+89
View File
@@ -1,4 +1,93 @@
[
{
"children": {
"improvements": ["Support more Text2Image from Qwen."]
},
"date": "2025-07-29",
"version": "1.105.1"
},
{
"children": {
"features": ["Implement API Key management functionality."]
},
"date": "2025-07-28",
"version": "1.105.0"
},
{
"children": {
"improvements": ["Fix setting window layout when in desktop was disappear."]
},
"date": "2025-07-28",
"version": "1.104.5"
},
{
"children": {
"improvements": ["Fix setting window layout size, update i18n."]
},
"date": "2025-07-28",
"version": "1.104.4"
},
{
"children": {
"improvements": ["Add Gemini 2.5 Flash-Lite GA model."]
},
"date": "2025-07-26",
"version": "1.104.3"
},
{
"children": {
"fixes": ["Fix update hotkey invalid when input mod in desktop."]
},
"date": "2025-07-26",
"version": "1.104.2"
},
{
"children": {
"fixes": [
"Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider."
]
},
"date": "2025-07-25",
"version": "1.104.1"
},
{
"children": {
"features": ["Support custom hotkey on desktop."]
},
"date": "2025-07-24",
"version": "1.104.0"
},
{
"children": {
"fixes": ["Fix chat stream in desktop and update shortcut."],
"improvements": [
"Add cached token count to usage of GoogleAI and VertexAI, fix desktop titlebar style in window, fix sub topic width in md responsive."
]
},
"date": "2025-07-24",
"version": "1.103.2"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-07-23",
"version": "1.103.1"
},
{
"children": {
"features": ["Add Qwen image generation capabilities."]
},
"date": "2025-07-22",
"version": "1.103.0"
},
{
"children": {
"improvements": ["Update tray icon."]
},
"date": "2025-07-22",
"version": "1.102.4"
},
{
"children": {
"fixes": ["Remove debug logging from ModelRuntime and async caller."]
@@ -1,36 +0,0 @@
# 添加新的 AI 图像模型
## 兼容 openai 请求格式的模型
指的是可以使用 openai SDK 进行请求,并且请求参数和和返回值和 dall-e 以及 gpt-image-x 系列一致。
以智谱的 CogView-4 为例,它是一个兼容 openai 请求格式的模型,可以按照以下步骤添加:
1. 在对应的 ai models 文件 `src/config/aiModels/zhipu.ts` 中,添加模型配置,例如:
```ts
const zhipuImageModels: AIImageModelCard[] = [
// 添加模型配置
// https://bigmodel.cn/dev/howuse/image-generation-model/cogview-4
{
description:
'CogView-4 是智谱首个支持生成汉字的开源文生图模型,在语义理解、图像生成质量、中英文字生成能力等方面全面提升,支持任意长度的中英双语输入,能够生成在给定范围内的任意分辨率图像。',
displayName: 'CogView-4',
enabled: true,
id: 'cogview-4',
parameters: {
prompt: {
default: '',
},
size: {
default: '1024x1024',
enum: ['1024x1024', '768x1344', '864x1152', '1344x768', '1152x864', '1440x720', '720x1440'],
},
},
releasedAt: '2025-03-04',
type: 'image',
},
];
```
2. 执行下 `npx i18n` 命令,更新模型描述的翻译文件
@@ -0,0 +1,162 @@
# Adding New Image Models
> Learn more about the AI image generation modal design in the [AI Image Generation Modal Design Discussion](https://github.com/lobehub/lobe-chat/discussions/7442)
## Parameter Standardization
All image generation models must use the standard parameters defined in `src/libs/standard-parameters/index.ts`. This ensures parameter consistency across different Providers, creating a more unified user experience.
**Supported Standard Parameters**:
- `prompt` (required): Text prompt for image generation
- `aspectRatio`: Aspect ratio (e.g., "16:9", "1:1")
- `width` / `height`: Image dimensions
- `size`: Preset dimensions (e.g., "1024x1024")
- `seed`: Random seed
- `steps`: Generation steps
- `cfg`: Guidance scale
- For other parameters, please check the source file
## OpenAI Compatible Models
These models can be requested using the OpenAI SDK, with request parameters and return values consistent with DALL-E and GPT-Image-X series.
Taking Zhipu's CogView-4 as an example, which is an OpenAI-compatible model, you can add it by adding the model configuration in the corresponding AI models file `src/config/aiModels/zhipu.ts`:
```ts
const zhipuImageModels: AIImageModelCard[] = [
// Add model configuration
// https://bigmodel.cn/dev/howuse/image-generation-model/cogview-4
{
description:
'CogView-4 is the first open-source text-to-image model from Zhipu that supports Chinese character generation, with comprehensive improvements in semantic understanding, image generation quality, and Chinese-English text generation capabilities.',
displayName: 'CogView-4',
enabled: true,
id: 'cogview-4',
parameters: {
prompt: {
default: '',
},
size: {
default: '1024x1024',
enum: ['1024x1024', '768x1344', '864x1152', '1344x768', '1152x864', '1440x720', '720x1440'],
},
},
releasedAt: '2025-03-04',
type: 'image',
},
];
```
## Non-OpenAI Compatible Models
For image generation models that are not compatible with OpenAI format, you need to implement a custom `createImage` method. There are two main implementation approaches:
### Method 1: Using OpenAI Compatible Factory
Most Providers use `openaiCompatibleFactory` for OpenAI compatibility. You can pass in a custom `createImage` function (reference [PR #8534](https://github.com/lobehub/lobe-chat/pull/8534)).
**Implementation Steps**:
1. **Read Provider documentation and standard parameter definitions**
- Review the Provider's image generation API documentation to understand request and response formats
- Read `src/libs/standard-parameters/index.ts` to understand supported parameters
- Add image model configuration in the corresponding AI models file
2. **Implement custom createImage method**
- Create a standalone image generation function that accepts standard parameters
- Convert standard parameters to Provider-specific format
- Call the Provider's image generation API
- Return a unified response format (imageUrl and optional width/height)
3. **Add tests**
- Write unit tests covering success scenarios
- Test various error cases and edge conditions
**Code Example**:
```ts
// src/libs/model-runtime/provider-name/createImage.ts
export const createProviderImage = async (
payload: ImageGenerationPayload,
options: any,
): Promise<ImageGenerationResponse> => {
const { model, prompt, ...params } = payload;
// Call Provider's native API
const result = await callProviderAPI({
model,
prompt,
// Convert parameter format
custom_param: params.width,
// ...
});
// Return unified format
return {
created: Date.now(),
data: [{ url: result.imageUrl }],
};
};
```
```ts
// src/libs/model-runtime/provider-name/index.ts
export const LobeProviderAI = openaiCompatibleFactory({
constructorOptions: {
// ... other configurations
},
createImage: createProviderImage, // Pass custom implementation
provider: ModelProvider.ProviderName,
});
```
### Method 2: Direct Implementation in Provider Class
If your Provider has an independent class implementation, you can directly add the `createImage` method in the class (reference [PR #8503](https://github.com/lobehub/lobe-chat/pull/8503)).
**Implementation Steps**:
1. **Read Provider documentation and standard parameter definitions**
- Review the Provider's image generation API documentation
- Read `src/libs/standard-parameters/index.ts`
- Add image model configuration in the corresponding AI models file
2. **Implement createImage method in Provider class**
- Add the `createImage` method directly in the class
- Handle parameter conversion and API calls
- Return a unified response format
3. **Add tests**
- Write comprehensive test cases for the new method
**Code Example**:
```ts
// src/libs/model-runtime/provider-name/index.ts
export class LobeProviderAI {
async createImage(
payload: ImageGenerationPayload,
options?: ChatStreamCallbacks,
): Promise<ImageGenerationResponse> {
const { model, prompt, ...params } = payload;
// Call native API and handle response
const result = await this.client.generateImage({
model,
prompt,
// Parameter conversion
});
return {
created: Date.now(),
data: [{ url: result.url }],
};
}
}
```
### Important Notes
- **Testing Requirements**: Add comprehensive unit tests for custom implementations, ensuring coverage of success scenarios and various error cases
- **Error Handling**: Use `AgentRuntimeError` consistently for error wrapping to maintain error message consistency
@@ -0,0 +1,162 @@
# 添加新的图像模型
> 了解更多关于 AI 绘画模态的设计,请参考 [AI 绘画模态设计讨论](https://github.com/lobehub/lobe-chat/discussions/7442)
## 参数标准化
所有图像生成模型都必须使用 `src/libs/standard-parameters/index.ts` 中定义的标准参数。这确保了不同 Provider 之间的参数一致性,让用户体验更加统一。
**支持的标准参数**
- `prompt` (必需):生成图像的提示词
- `aspectRatio`:宽高比(如 "16:9", "1:1"
- `width` / `height`:图像宽高
- `size`:预设尺寸(如 "1024x1024"
- `seed`:随机种子
- `steps`:生成步数
- `cfg`:引导缩放
- 其他参数请查看源文件
## 兼容 OpenAI 请求格式的模型
指的是可以使用 openai SDK 进行请求,并且请求参数和和返回值和 dall-e 以及 gpt-image-x 系列一致。
以智谱的 CogView-4 为例,它是一个兼容 openai 请求格式的模型。你只需要在对应的 ai models 文件 `src/config/aiModels/zhipu.ts` 中,添加模型配置,例如:
```ts
const zhipuImageModels: AIImageModelCard[] = [
// 添加模型配置
// https://bigmodel.cn/dev/howuse/image-generation-model/cogview-4
{
description:
'CogView-4 是智谱首个支持生成汉字的开源文生图模型,在语义理解、图像生成质量、中英文字生成能力等方面全面提升,支持任意长度的中英双语输入,能够生成在给定范围内的任意分辨率图像。',
displayName: 'CogView-4',
enabled: true,
id: 'cogview-4',
parameters: {
prompt: {
default: '',
},
size: {
default: '1024x1024',
enum: ['1024x1024', '768x1344', '864x1152', '1344x768', '1152x864', '1440x720', '720x1440'],
},
},
releasedAt: '2025-03-04',
type: 'image',
},
];
```
## 不兼容 OpenAI 请求格式的模型
对于不兼容 OpenAI 格式的图像生成模型,需要实现自定义的 `createImage` 方法。有两种主要实现方式:
### 方式一:使用 OpenAI Compatible Factory
大部分 Provider 都使用 `openaiCompatibleFactory` 来兼容 OpenAI,可以通过传入自定义的 `createImage` 函数(参考 [PR #8534](https://github.com/lobehub/lobe-chat/pull/8534))。
**实现步骤**
1. **阅读 Provider 官方文档和标准参数定义**
- 查看 Provider 的图像生成 API 文档,了解请求格式和响应格式
- 阅读 `src/libs/standard-parameters/index.ts`,了解支持的参数
- 在对应的 ai models 文件中增加 image model 配置
2. **实现自定义的 createImage 方法**
- 创建独立的图像生成函数,接受标准生图参数
- 将标准参数转换为 Provider 特定的格式
- 调用 Provider 的生图接口
- 返回统一格式的响应(imageUrl 和可选的宽高)
3. **补充测试**
- 编写单元测试覆盖成功场景
- 测试各种错误情况和边界条件
**代码示例**
```ts
// src/libs/model-runtime/provider-name/createImage.ts
export const createProviderImage = async (
payload: ImageGenerationPayload,
options: any,
): Promise<ImageGenerationResponse> => {
const { model, prompt, ...params } = payload;
// 调用 Provider 的原生 API
const result = await callProviderAPI({
model,
prompt,
// 转换参数格式
custom_param: params.width,
// ...
});
// 返回统一格式
return {
created: Date.now(),
data: [{ url: result.imageUrl }],
};
};
```
```ts
// src/libs/model-runtime/provider-name/index.ts
export const LobeProviderAI = openaiCompatibleFactory({
constructorOptions: {
// ... 其他配置
},
createImage: createProviderImage, // 传入自定义实现
provider: ModelProvider.ProviderName,
});
```
### 方式二:在 Provider 类中直接实现
如果你的 Provider 有独立的类实现,可以直接在类中添加 `createImage` 方法(参考 [PR #8503](https://github.com/lobehub/lobe-chat/pull/8503))。
**实现步骤**
1. **阅读 Provider 官方文档和标准参数定义**
- 查看 Provider 的图像生成 API 文档
- 阅读 `src/libs/standard-parameters/index.ts`
- 在对应的 ai models 文件中增加 image model 配置
2. **在 Provider 类中实现 createImage 方法**
- 直接在类中添加 `createImage` 方法
- 处理参数转换和 API 调用
- 返回统一格式的响应
3. **补充测试**
- 为新方法编写完整的测试用例
**代码示例**
```ts
// src/libs/model-runtime/provider-name/index.ts
export class LobeProviderAI {
async createImage(
payload: ImageGenerationPayload,
options?: ChatStreamCallbacks,
): Promise<ImageGenerationResponse> {
const { model, prompt, ...params } = payload;
// 调用原生 API 并处理响应
const result = await this.client.generateImage({
model,
prompt,
// 参数转换
});
return {
created: Date.now(),
data: [{ url: result.url }],
};
}
}
```
### 重要注意事项
- **测试要求**:为自定义实现添加完整的单元测试,确保覆盖成功场景和各种错误情况
- **错误处理**:统一使用 `AgentRuntimeError` 进行错误封装,保持错误信息的一致性
+1 -1
View File
@@ -16,7 +16,7 @@ tags:
# Desktop Application
<Image alt={'Desktop Application'} borderless cover src={'https://github.com/user-attachments/assets/a7bac8d3-ea96-4000-bb39-fadc9b610f96'}> />
<Image alt={'Desktop Application'} borderless cover src={'https://github.com/user-attachments/assets/a7bac8d3-ea96-4000-bb39-fadc9b610f96'} />
**Peak Performance, Zero Distractions**
+1 -1
View File
@@ -16,7 +16,7 @@ tags:
# MCP Marketplace
<Image alt={'MCP Marketplace'} borderless cover src={'https://github.com/user-attachments/assets/bb114f9f-24c5-4000-a984-c10d187da5a0'}> />
<Image alt={'MCP Marketplace'} borderless cover src={'https://github.com/user-attachments/assets/bb114f9f-24c5-4000-a984-c10d187da5a0'} />
**Discover, Connect, Expand**
+1 -1
View File
@@ -56,7 +56,7 @@ tags:
<Image alt={'Enter API Key'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/fa056feecba0133c76abe1ad12706c05.png'} />
- Paste the API key you obtained.
- Choose a Fal model (e.g. `fal-ai/flux-pro`, `fal-ai/kling-video`, `fal-ai/hidream-i1-fast`) for image or video generation.
- Choose a Fal model (e.g. `Flux.1 Schnell`, `Flux.1 Kontext Dev`) for image or video generation.
<Image alt={'Select Fal model for media generation'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/7560502f31b8500032922103fc22e69b.png'} />
+1 -1
View File
@@ -56,7 +56,7 @@ tags:
<Image alt={'填入 API 密钥'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/fa056feecba0133c76abe1ad12706c05.png'} />
- 粘贴获取到的 API Key
- 选择一个 Fal 模型(如 `fal-ai/flux-pro`、`fal-ai/kling-video`、`fal-ai/hidream-i1-fast`)用于图像或视频生成。
- 选择一个 Fal 模型(如 `Flux.1 Schnell`、`Flux.1 Kontext Dev`)用于图像或视频生成。
<Image alt={'选择 Fal 模型进行媒体生成'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/7560502f31b8500032922103fc22e69b.png'} />
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "تم الإنشاء تلقائيًا",
"copy": "نسخ",
"copyError": "فشل النسخ",
"copySuccess": "تم نسخ مفتاح API إلى الحافظة",
"enterPlaceholder": "الرجاء الإدخال",
"hide": "إخفاء",
"neverExpires": "لا تنتهي صلاحيتها أبدًا",
"neverUsed": "لم يُستخدم أبدًا",
"show": "عرض"
},
"form": {
"fields": {
"expiresAt": {
"label": "تاريخ الانتهاء",
"placeholder": "لا تنتهي صلاحيتها أبدًا"
},
"name": {
"label": "الاسم",
"placeholder": "الرجاء إدخال اسم مفتاح API"
}
},
"submit": "إنشاء",
"title": "إنشاء مفتاح API"
},
"list": {
"actions": {
"create": "إنشاء مفتاح API",
"delete": "حذف",
"deleteConfirm": {
"actions": {
"cancel": "إلغاء",
"ok": "تأكيد"
},
"content": "هل أنت متأكد من حذف هذا المفتاح؟",
"title": "تأكيد العملية"
}
},
"columns": {
"actions": "الإجراءات",
"expiresAt": "تاريخ الانتهاء",
"key": "المفتاح",
"lastUsedAt": "آخر استخدام",
"name": "الاسم",
"status": "حالة التفعيل"
},
"title": "قائمة مفاتيح API"
},
"validation": {
"required": "لا يمكن أن يكون المحتوى فارغًا"
}
},
"date": {
"prevMonth": "الشهر الماضي",
"recent30Days": "آخر 30 يومًا"
@@ -89,6 +142,7 @@
"words": "كلمات"
},
"tab": {
"apikey": "إدارة مفاتيح API",
"profile": "الملف الشخصي",
"security": "الأمان",
"stats": "الإحصائيات"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "مسح الرسائل والملفات المرفوعة في المحادثة الحالية",
"title": "مسح رسائل المحادثة"
},
"desktop": {
"openSettings": {
"desc": "افتح صفحة إعدادات التطبيق",
"title": "إعدادات التطبيق"
},
"showApp": {
"desc": "مفتاح اختصار عام لإظهار أو إخفاء النافذة الرئيسية",
"title": "إظهار/إخفاء النافذة الرئيسية"
}
},
"editMessage": {
"desc": "الدخول إلى وضع التحرير عن طريق الضغط على مفتاح Alt والنقر المزدوج على الرسالة",
"title": "تحرير الرسالة"
@@ -19,10 +29,6 @@
"desc": "عرض جميع تعليمات استخدام الاختصارات",
"title": "فتح مساعدة الاختصارات"
},
"openSettings": {
"desc": "فتح صفحة إعدادات التطبيق",
"title": "إعدادات التطبيق"
},
"regenerateMessage": {
"desc": "إعادة توليد آخر رسالة",
"title": "إعادة توليد الرسالة"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash هو نموذج Google الأكثر فعالية من حيث التكلفة، ويوفر وظائف شاملة."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite هو أصغر وأفضل نموذج من حيث التكلفة من Google، مصمم للاستخدام على نطاق واسع."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview هو أصغر وأكفأ نموذج من Google، مصمم للاستخدام واسع النطاق."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "نموذج v0-1.5-md مناسب للمهام اليومية وتوليد واجهات المستخدم (UI)"
},
"wanx2.1-t2i-turbo": {
"description": "نموذج توليد الصور التابع لشركة علي بابا كلاود Tongyi"
},
"whisper-1": {
"description": "نموذج التعرف على الصوت العام، يدعم التعرف على الصوت بعدة لغات، الترجمة الصوتية، والتعرف على اللغة."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "يتعارض مع اختصارات لوحة المفاتيح الحالية",
"errors": {
"CONFLICT": "تعارض في اختصار لوحة المفاتيح: هذا الاختصار مستخدم بالفعل من قبل وظيفة أخرى",
"INVALID_FORMAT": "تنسيق اختصار لوحة المفاتيح غير صالح: يرجى استخدام التنسيق الصحيح (مثل CommandOrControl+E)",
"INVALID_ID": "معرف اختصار لوحة المفاتيح غير صالح",
"NO_MODIFIER": "يجب أن يحتوي اختصار لوحة المفاتيح على مفتاح تعديل (Ctrl، Alt، Shift، إلخ)",
"SYSTEM_OCCUPIED": "اختصار لوحة المفاتيح مستخدم من قبل النظام أو تطبيقات أخرى",
"UNKNOWN": "فشل التحديث: خطأ غير معروف"
},
"group": {
"conversation": "المحادثة",
"desktop": "سطح المكتب",
"essential": "أساسي"
},
"invalidCombination": "يجب أن تحتوي اختصارات لوحة المفاتيح على مفتاح تعديل واحد على الأقل (Ctrl، Alt، Shift) ومفتاح عادي واحد",
"record": "اضغط على المفتاح لتسجيل اختصار لوحة المفاتيح",
"reset": "إعادة تعيين إلى اختصارات لوحة المفاتيح الافتراضية",
"title": "اختصارات لوحة المفاتيح"
"title": "اختصارات لوحة المفاتيح",
"updateError": "فشل تحديث اختصار لوحة المفاتيح: خطأ في الشبكة أو النظام",
"updateSuccess": "تم تحديث اختصار لوحة المفاتيح بنجاح"
},
"llm": {
"aesGcm": "سيتم استخدام خوارزمية التشفير <1>AES-GCM</1> لتشفير مفتاحك وعنوان الوكيل",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Автоматично генериран",
"copy": "Копирай",
"copyError": "Грешка при копиране",
"copySuccess": "API ключът е копиран в клипборда",
"enterPlaceholder": "Моля, въведете",
"hide": "Скрий",
"neverExpires": "Никога не изтича",
"neverUsed": "Никога не е използван",
"show": "Покажи"
},
"form": {
"fields": {
"expiresAt": {
"label": "Дата на изтичане",
"placeholder": "Никога не изтича"
},
"name": {
"label": "Име",
"placeholder": "Моля, въведете име на API ключ"
}
},
"submit": "Създай",
"title": "Създаване на API ключ"
},
"list": {
"actions": {
"create": "Създай API ключ",
"delete": "Изтрий",
"deleteConfirm": {
"actions": {
"cancel": "Отказ",
"ok": "Потвърди"
},
"content": "Сигурни ли сте, че искате да изтриете този API ключ?",
"title": "Потвърждение на действие"
}
},
"columns": {
"actions": "Действия",
"expiresAt": "Дата на изтичане",
"key": "Ключ",
"lastUsedAt": "Последна употреба",
"name": "Име",
"status": "Статус на активиране"
},
"title": "Списък с API ключове"
},
"validation": {
"required": "Полето не може да бъде празно"
}
},
"date": {
"prevMonth": "Миналия месец",
"recent30Days": "Последните 30 дни"
@@ -89,6 +142,7 @@
"words": "Думи"
},
"tab": {
"apikey": "Управление на API ключове",
"profile": "Профил",
"security": "Сигурност",
"stats": "Статистика"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Изтриване на текущите съобщения и качените файлове в сесията",
"title": "Изтриване на съобщенията в сесията"
},
"desktop": {
"openSettings": {
"desc": "Отворете страницата с настройки на приложението",
"title": "Настройки на приложението"
},
"showApp": {
"desc": "Глобална клавишна комбинация за показване или скриване на главния прозорец",
"title": "Показване/скриване на главния прозорец"
}
},
"editMessage": {
"desc": "Влезте в режим на редактиране, като задържите Alt и два пъти кликнете върху съобщението",
"title": "Редактиране на съобщение"
@@ -19,10 +29,6 @@
"desc": "Прегледайте инструкциите за използване на всички клавишни комбинации",
"title": "Отворете помощта за клавишни комбинации"
},
"openSettings": {
"desc": "Отворете страницата с настройки на приложението",
"title": "Настройки на приложението"
},
"regenerateMessage": {
"desc": "Прегенерирайте последното съобщение",
"title": "Прегенериране на съобщение"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash е най-ефективният модел на Google, предлагащ пълна функционалност."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite е най-малкият и най-ефективен модел на Google, създаден специално за масово използване."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview е най-малкият и най-ефективен модел на Google, проектиран за мащабна употреба."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "Моделът v0-1.5-md е подходящ за ежедневни задачи и генериране на потребителски интерфейс (UI)"
},
"wanx2.1-t2i-turbo": {
"description": "Модел за генериране на изображения от текст на Alibaba Cloud Tongyi"
},
"whisper-1": {
"description": "Универсален модел за разпознаване на реч, поддържащ многоезично разпознаване на реч, превод на реч и разпознаване на език."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "Конфликт с текущите клавишни комбинации",
"errors": {
"CONFLICT": "Конфликт на клавишната комбинация: тази комбинация вече е заета от друга функция",
"INVALID_FORMAT": "Невалиден формат на клавишната комбинация: моля, използвайте правилния формат (например CommandOrControl+E)",
"INVALID_ID": "Невалиден идентификатор на клавишната комбинация",
"NO_MODIFIER": "Клавишната комбинация трябва да съдържа модификатор (Ctrl, Alt, Shift и др.)",
"SYSTEM_OCCUPIED": "Клавишната комбинация е заета от системата или друго приложение",
"UNKNOWN": "Актуализацията не бе успешна: неизвестна грешка"
},
"group": {
"conversation": "Разговор",
"desktop": "Настолен",
"essential": "Основен"
},
"invalidCombination": "Клавишната комбинация трябва да съдържа поне един модификатор (Ctrl, Alt, Shift) и един обикновен клавиш",
"record": "Натиснете клавиш, за да запишете клавишна комбинация",
"reset": "Нулиране до подразбиращите се клавишни комбинации",
"title": "Бързи клавиши"
"title": "Бързи клавиши",
"updateError": "Актуализацията на клавишната комбинация не бе успешна: мрежова или системна грешка",
"updateSuccess": "Актуализацията на клавишната комбинация бе успешна"
},
"llm": {
"aesGcm": "Вашият ключ и адрес на агента ще бъдат криптирани с алгоритъма за криптиране <1>AES-GCM</1>",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Automatisch generiert",
"copy": "Kopieren",
"copyError": "Kopieren fehlgeschlagen",
"copySuccess": "API-Schlüssel wurde in die Zwischenablage kopiert",
"enterPlaceholder": "Bitte eingeben",
"hide": "Verbergen",
"neverExpires": "Läuft nie ab",
"neverUsed": "Nie verwendet",
"show": "Anzeigen"
},
"form": {
"fields": {
"expiresAt": {
"label": "Ablaufdatum",
"placeholder": "Läuft nie ab"
},
"name": {
"label": "Name",
"placeholder": "Bitte API-Schlüsselname eingeben"
}
},
"submit": "Erstellen",
"title": "API-Schlüssel erstellen"
},
"list": {
"actions": {
"create": "API-Schlüssel erstellen",
"delete": "Löschen",
"deleteConfirm": {
"actions": {
"cancel": "Abbrechen",
"ok": "Bestätigen"
},
"content": "Möchten Sie diesen API-Schlüssel wirklich löschen?",
"title": "Bestätigung"
}
},
"columns": {
"actions": "Aktionen",
"expiresAt": "Ablaufdatum",
"key": "Schlüssel",
"lastUsedAt": "Letzte Verwendung",
"name": "Name",
"status": "Aktivierungsstatus"
},
"title": "API-Schlüssel Liste"
},
"validation": {
"required": "Inhalt darf nicht leer sein"
}
},
"date": {
"prevMonth": "Letzter Monat",
"recent30Days": "Letzte 30 Tage"
@@ -89,6 +142,7 @@
"words": "Wörter"
},
"tab": {
"apikey": "API-Schlüssel Verwaltung",
"profile": "Profil",
"security": "Sicherheit",
"stats": "Statistiken"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Aktuelle Nachrichten und hochgeladene Dateien im Gespräch löschen",
"title": "Gesprächsnachrichten löschen"
},
"desktop": {
"openSettings": {
"desc": "Öffnet die Anwendungseinstellungsseite",
"title": "Anwendungseinstellungen"
},
"showApp": {
"desc": "Globale Tastenkombination zum Anzeigen oder Verbergen des Hauptfensters",
"title": "Hauptfenster anzeigen/verbergen"
}
},
"editMessage": {
"desc": "Treten Sie in den Bearbeitungsmodus, indem Sie die Alt-Taste gedrückt halten und auf die Nachricht doppelklicken",
"title": "Nachricht bearbeiten"
@@ -19,10 +29,6 @@
"desc": "Anleitung zur Verwendung aller Tastenkombinationen anzeigen",
"title": "Tastenkombinationshilfe öffnen"
},
"openSettings": {
"desc": "Öffnen Sie die Anwendungseinstellungen",
"title": "Anwendungseinstellungen"
},
"regenerateMessage": {
"desc": "Die letzte Nachricht neu generieren",
"title": "Nachricht neu generieren"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash ist Googles kosteneffizientestes Modell und bietet umfassende Funktionen."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite ist Googles kleinstes und kosteneffizientestes Modell, das speziell für den großflächigen Einsatz entwickelt wurde."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview ist Googles kleinstes und kosteneffizientestes Modell, speziell für den großflächigen Einsatz konzipiert."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "Das Modell v0-1.5-md ist für alltägliche Aufgaben und die Generierung von Benutzeroberflächen (UI) geeignet"
},
"wanx2.1-t2i-turbo": {
"description": "Text-zu-Bild-Modell von Aliyun Tongyi"
},
"whisper-1": {
"description": "Universelles Spracherkennungsmodell, unterstützt mehrsprachige Spracherkennung, Sprachübersetzung und Spracherkennung."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "Konflikte mit bestehenden Tastenkombinationen",
"errors": {
"CONFLICT": "Tastenkonflikt: Diese Tastenkombination wird bereits von einer anderen Funktion verwendet",
"INVALID_FORMAT": "Ungültiges Tastenkürzel-Format: Bitte verwenden Sie das korrekte Format (z. B. CommandOrControl+E)",
"INVALID_ID": "Ungültige Tastenkürzel-ID",
"NO_MODIFIER": "Das Tastenkürzel muss einen Modifikatortaste enthalten (Strg, Alt, Shift usw.)",
"SYSTEM_OCCUPIED": "Das Tastenkürzel wird vom System oder einer anderen Anwendung verwendet",
"UNKNOWN": "Aktualisierung fehlgeschlagen: Unbekannter Fehler"
},
"group": {
"conversation": "Gespräch",
"desktop": "Desktop",
"essential": "Grundlegend"
},
"invalidCombination": "Die Tastenkombination muss mindestens einen Modifikatortaste (Strg, Alt, Umschalt) und eine normale Taste enthalten",
"record": "Drücken Sie eine Taste, um die Tastenkombination aufzuzeichnen",
"reset": "Auf die Standard-Tastenkombination zurücksetzen",
"title": "Tastenkombinationen"
"title": "Tastenkombinationen",
"updateError": "Tastenkürzel-Aktualisierung fehlgeschlagen: Netzwerk- oder Systemfehler",
"updateSuccess": "Tastenkürzel erfolgreich aktualisiert"
},
"llm": {
"aesGcm": "Ihr Schlüssel und Ihre Proxy-Adresse werden mit dem <1>AES-GCM</1> Verschlüsselungsalgorithmus verschlüsselt.",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Auto-generated",
"copy": "Copy",
"copyError": "Copy failed",
"copySuccess": "API Key copied to clipboard",
"enterPlaceholder": "Please enter",
"hide": "Hide",
"neverExpires": "Never expires",
"neverUsed": "Never used",
"show": "Show"
},
"form": {
"fields": {
"expiresAt": {
"label": "Expiration Date",
"placeholder": "Never expires"
},
"name": {
"label": "Name",
"placeholder": "Please enter API Key name"
}
},
"submit": "Create",
"title": "Create API Key"
},
"list": {
"actions": {
"create": "Create API Key",
"delete": "Delete",
"deleteConfirm": {
"actions": {
"cancel": "Cancel",
"ok": "Confirm"
},
"content": "Are you sure you want to delete this API Key?",
"title": "Confirm Action"
}
},
"columns": {
"actions": "Actions",
"expiresAt": "Expiration Date",
"key": "Key",
"lastUsedAt": "Last Used",
"name": "Name",
"status": "Enabled Status"
},
"title": "API Key List"
},
"validation": {
"required": "This field cannot be empty"
}
},
"date": {
"prevMonth": "Last Month",
"recent30Days": "Last 30 Days"
@@ -89,6 +142,7 @@
"words": "Total Words"
},
"tab": {
"apikey": "API Key Management",
"profile": "Profile",
"security": "Security",
"stats": "Statistics"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Clear the messages and uploaded files from the current conversation",
"title": "Clear Conversation Messages"
},
"desktop": {
"openSettings": {
"desc": "Open the application settings page",
"title": "Application Settings"
},
"showApp": {
"desc": "Toggle the main window visibility with a global shortcut",
"title": "Show/Hide Main Window"
}
},
"editMessage": {
"desc": "Enter edit mode by holding Alt and double-clicking the message",
"title": "Edit Message"
@@ -19,10 +29,6 @@
"desc": "View instructions for all keyboard shortcuts",
"title": "Open Hotkey Help"
},
"openSettings": {
"desc": "Open the application settings page",
"title": "Application Settings"
},
"regenerateMessage": {
"desc": "Regenerate the last message",
"title": "Regenerate Message"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash is Google's most cost-effective model, offering comprehensive capabilities."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite is Google's smallest and most cost-effective model, designed for large-scale use."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview is Google's smallest and most cost-efficient model, designed for large-scale usage."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "The v0-1.5-md model is suitable for everyday tasks and user interface (UI) generation."
},
"wanx2.1-t2i-turbo": {
"description": "Text-to-image model under Alibaba Cloud Tongyi"
},
"whisper-1": {
"description": "A general-purpose speech recognition model supporting multilingual speech recognition, speech translation, and language identification."
},
+13 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "Conflicts with existing hotkeys",
"errors": {
"CONFLICT": "Hotkey conflict: This hotkey is already assigned to another function",
"INVALID_FORMAT": "Invalid hotkey format: Please use the correct format (e.g., CommandOrControl+E)",
"INVALID_ID": "Invalid hotkey ID",
"NO_MODIFIER": "Hotkey must include a modifier key (Ctrl, Alt, Shift, etc.)",
"SYSTEM_OCCUPIED": "Hotkey is occupied by the system or another application",
"UNKNOWN": "Update failed: Unknown error"
},
"group": {
"conversation": "Conversation",
"desktop": "Desktop",
"essential": "Essential"
},
"invalidCombination": "The hotkey must include at least one modifier key (Ctrl, Alt, Shift) and one regular key",
"record": "Press a key to record the hotkey",
"reset": "Reset to default hotkeys",
"title": "Hotkeys"
"title": "Hotkeys",
"updateError": "Failed to update hotkey: Network or system error",
"updateSuccess": "Hotkey updated successfully"
},
"llm": {
"aesGcm": "Your keys and proxy address will be encrypted using the <1>AES-GCM</1> encryption algorithm",
@@ -524,6 +535,7 @@
"experiment": "Experiment",
"hotkey": "Hotkeys",
"llm": "Language Model",
"plugin": "Plugin Management",
"provider": "AI Service Provider",
"proxy": "Network Proxy",
"storage": "Data Storage",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Generado automáticamente",
"copy": "Copiar",
"copyError": "Error al copiar",
"copySuccess": "Clave API copiada al portapapeles",
"enterPlaceholder": "Por favor ingrese",
"hide": "Ocultar",
"neverExpires": "Nunca expira",
"neverUsed": "Nunca usado",
"show": "Mostrar"
},
"form": {
"fields": {
"expiresAt": {
"label": "Fecha de expiración",
"placeholder": "Nunca expira"
},
"name": {
"label": "Nombre",
"placeholder": "Por favor ingrese el nombre de la clave API"
}
},
"submit": "Crear",
"title": "Crear Clave API"
},
"list": {
"actions": {
"create": "Crear Clave API",
"delete": "Eliminar",
"deleteConfirm": {
"actions": {
"cancel": "Cancelar",
"ok": "Confirmar"
},
"content": "¿Está seguro de eliminar esta clave API?",
"title": "Confirmar acción"
}
},
"columns": {
"actions": "Acciones",
"expiresAt": "Fecha de expiración",
"key": "Clave",
"lastUsedAt": "Último uso",
"name": "Nombre",
"status": "Estado"
},
"title": "Lista de Claves API"
},
"validation": {
"required": "El contenido no puede estar vacío"
}
},
"date": {
"prevMonth": "Último mes",
"recent30Days": "Últimos 30 días"
@@ -89,6 +142,7 @@
"words": "Palabras"
},
"tab": {
"apikey": "Gestión de Claves API",
"profile": "Perfil",
"security": "Seguridad",
"stats": "Estadísticas"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Eliminar los mensajes y archivos subidos de la conversación actual",
"title": "Eliminar mensajes de la conversación"
},
"desktop": {
"openSettings": {
"desc": "Abrir la página de configuración de la aplicación",
"title": "Configuración de la aplicación"
},
"showApp": {
"desc": "Mostrar u ocultar la ventana principal mediante un atajo global",
"title": "Mostrar/Ocultar ventana principal"
}
},
"editMessage": {
"desc": "Entrar en modo de edición manteniendo presionada la tecla Alt y haciendo doble clic en el mensaje",
"title": "Editar mensaje"
@@ -19,10 +29,6 @@
"desc": "Ver las instrucciones de uso de todos los atajos de teclado",
"title": "Abrir ayuda de atajos de teclado"
},
"openSettings": {
"desc": "Abrir la página de configuración de la aplicación",
"title": "Configuración de la aplicación"
},
"regenerateMessage": {
"desc": "Regenerar el último mensaje",
"title": "Regenerar mensaje"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash es el modelo de mejor relación calidad-precio de Google, que ofrece funcionalidades completas."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite es el modelo más pequeño y rentable de Google, diseñado para un uso a gran escala."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview es el modelo más pequeño y con mejor relación calidad-precio de Google, diseñado para un uso a gran escala."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "El modelo v0-1.5-md es adecuado para tareas cotidianas y generación de interfaces de usuario (UI)"
},
"wanx2.1-t2i-turbo": {
"description": "Modelo de generación de imágenes de texto a imagen de Tongyi de Alibaba Cloud"
},
"whisper-1": {
"description": "Modelo universal de reconocimiento de voz que soporta reconocimiento de voz multilingüe, traducción de voz y detección de idioma."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "Conflicto con las teclas de acceso rápido existentes",
"errors": {
"CONFLICT": "Conflicto de atajo: este atajo ya está asignado a otra función",
"INVALID_FORMAT": "Formato de atajo inválido: por favor use el formato correcto (por ejemplo, CommandOrControl+E)",
"INVALID_ID": "ID de atajo inválido",
"NO_MODIFIER": "El atajo debe incluir una tecla modificadora (Ctrl, Alt, Shift, etc.)",
"SYSTEM_OCCUPIED": "El atajo está ocupado por el sistema u otra aplicación",
"UNKNOWN": "Error al actualizar: error desconocido"
},
"group": {
"conversation": "Conversación",
"desktop": "Escritorio",
"essential": "Esencial"
},
"invalidCombination": "La combinación de teclas de acceso rápido debe incluir al menos una tecla modificadora (Ctrl, Alt, Shift) y una tecla normal",
"record": "Presiona una tecla para grabar la tecla de acceso rápido",
"reset": "Restablecer a las teclas de acceso rápido predeterminadas",
"title": "Atajos de teclado"
"title": "Atajos de teclado",
"updateError": "Error al actualizar el atajo: problema de red o del sistema",
"updateSuccess": "Atajo actualizado con éxito"
},
"llm": {
"aesGcm": "Su clave y dirección del agente se cifrarán utilizando el algoritmo de cifrado <1>AES-GCM</1>",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "تولید خودکار",
"copy": "کپی",
"copyError": "کپی ناموفق بود",
"copySuccess": "کلید API به کلیپ‌بورد کپی شد",
"enterPlaceholder": "لطفاً وارد کنید",
"hide": "مخفی کردن",
"neverExpires": "هرگز منقضی نمی‌شود",
"neverUsed": "هرگز استفاده نشده",
"show": "نمایش"
},
"form": {
"fields": {
"expiresAt": {
"label": "تاریخ انقضا",
"placeholder": "هرگز منقضی نمی‌شود"
},
"name": {
"label": "نام",
"placeholder": "لطفاً نام کلید API را وارد کنید"
}
},
"submit": "ایجاد",
"title": "ایجاد کلید API"
},
"list": {
"actions": {
"create": "ایجاد کلید API",
"delete": "حذف",
"deleteConfirm": {
"actions": {
"cancel": "لغو",
"ok": "تأیید"
},
"content": "آیا از حذف این کلید API مطمئن هستید؟",
"title": "تأیید عملیات"
}
},
"columns": {
"actions": "عملیات",
"expiresAt": "تاریخ انقضا",
"key": "کلید",
"lastUsedAt": "آخرین زمان استفاده",
"name": "نام",
"status": "وضعیت فعال"
},
"title": "فهرست کلیدهای API"
},
"validation": {
"required": "محتوا نباید خالی باشد"
}
},
"date": {
"prevMonth": "ماه گذشته",
"recent30Days": "۳۰ روز گذشته"
@@ -89,6 +142,7 @@
"words": "کلمات"
},
"tab": {
"apikey": "مدیریت کلید API",
"profile": "پروفایل",
"security": "امنیت",
"stats": "آمار"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "حذف پیام‌ها و فایل‌های بارگذاری شده در جلسه جاری",
"title": "حذف پیام‌های جلسه"
},
"desktop": {
"openSettings": {
"desc": "باز کردن صفحه تنظیمات برنامه",
"title": "تنظیمات برنامه"
},
"showApp": {
"desc": "نمایش یا پنهان کردن پنجره اصلی با کلید میانبر جهانی",
"title": "نمایش/پنهان کردن پنجره اصلی"
}
},
"editMessage": {
"desc": "با نگه داشتن کلید Alt و دوبار کلیک بر روی پیام وارد حالت ویرایش شوید",
"title": "ویرایش پیام"
@@ -19,10 +29,6 @@
"desc": "مشاهده تمام توضیحات استفاده از کلیدهای میانبر",
"title": "باز کردن راهنمای کلیدهای میانبر"
},
"openSettings": {
"desc": "صفحه تنظیمات برنامه را باز کنید",
"title": "تنظیمات برنامه"
},
"regenerateMessage": {
"desc": "آخرین پیام را دوباره تولید کنید",
"title": "تولید مجدد پیام"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash مدل با بهترین نسبت قیمت به کارایی گوگل است که امکانات جامع را ارائه می‌دهد."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite کوچک‌ترین و مقرون‌به‌صرفه‌ترین مدل گوگل است که برای استفاده در مقیاس وسیع طراحی شده است."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview کوچک‌ترین و مقرون‌به‌صرفه‌ترین مدل گوگل است که برای استفاده در مقیاس بزرگ طراحی شده است."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "مدل v0-1.5-md برای وظایف روزمره و تولید رابط کاربری (UI) مناسب است"
},
"wanx2.1-t2i-turbo": {
"description": "مدل تولید تصویر مبتنی بر متن زیرمجموعه‌ی علی‌بابا کلود Tongyi"
},
"whisper-1": {
"description": "مدل شناسایی گفتار عمومی که از شناسایی گفتار چندزبانه، ترجمه گفتار و شناسایی زبان پشتیبانی می‌کند."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "تداخل با کلیدهای میانبر موجود",
"errors": {
"CONFLICT": "تداخل کلید میانبر: این کلید میانبر قبلاً توسط عملکرد دیگری استفاده شده است",
"INVALID_FORMAT": "فرمت کلید میانبر نامعتبر است: لطفاً از فرمت صحیح استفاده کنید (مانند CommandOrControl+E)",
"INVALID_ID": "شناسه کلید میانبر نامعتبر است",
"NO_MODIFIER": "کلید میانبر باید شامل کلیدهای تغییر دهنده (Ctrl، Alt، Shift و غیره) باشد",
"SYSTEM_OCCUPIED": "کلید میانبر توسط سیستم یا برنامه‌های دیگر اشغال شده است",
"UNKNOWN": "به‌روزرسانی ناموفق بود: خطای ناشناخته"
},
"group": {
"conversation": "گفتگو",
"desktop": "نسخه دسکتاپ",
"essential": "اساسی"
},
"invalidCombination": "کلیدهای میانبر باید حداقل شامل یک کلید اصلاحی (Ctrl, Alt, Shift) و یک کلید معمولی باشند",
"record": "برای ضبط کلید میانبر، کلید را فشار دهید",
"reset": "بازنشانی به کلیدهای میانبر پیش‌فرض",
"title": "کلیدهای میانبر"
"title": "کلیدهای میانبر",
"updateError": "به‌روزرسانی کلید میانبر ناموفق بود: خطای شبکه یا سیستم",
"updateSuccess": "کلید میانبر با موفقیت به‌روزرسانی شد"
},
"llm": {
"aesGcm": "کلید و آدرس پروکسی شما با استفاده از الگوریتم رمزنگاری <1>AES-GCM</1> رمزگذاری خواهد شد",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Généré automatiquement",
"copy": "Copier",
"copyError": "Échec de la copie",
"copySuccess": "Clé API copiée dans le presse-papiers",
"enterPlaceholder": "Veuillez saisir",
"hide": "Cacher",
"neverExpires": "N'expire jamais",
"neverUsed": "Jamais utilisé",
"show": "Afficher"
},
"form": {
"fields": {
"expiresAt": {
"label": "Date d'expiration",
"placeholder": "N'expire jamais"
},
"name": {
"label": "Nom",
"placeholder": "Veuillez saisir le nom de la clé API"
}
},
"submit": "Créer",
"title": "Créer une clé API"
},
"list": {
"actions": {
"create": "Créer une clé API",
"delete": "Supprimer",
"deleteConfirm": {
"actions": {
"cancel": "Annuler",
"ok": "Confirmer"
},
"content": "Confirmez-vous la suppression de cette clé API ?",
"title": "Confirmation"
}
},
"columns": {
"actions": "Actions",
"expiresAt": "Date d'expiration",
"key": "Clé",
"lastUsedAt": "Dernière utilisation",
"name": "Nom",
"status": "Statut d'activation"
},
"title": "Liste des clés API"
},
"validation": {
"required": "Ce champ est obligatoire"
}
},
"date": {
"prevMonth": "Le mois dernier",
"recent30Days": "Les 30 derniers jours"
@@ -89,6 +142,7 @@
"words": "Mots"
},
"tab": {
"apikey": "Gestion des clés API",
"profile": "Profil",
"security": "Sécurité",
"stats": "Statistiques"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Effacer les messages de la session actuelle et les fichiers téléchargés",
"title": "Effacer les messages de la session"
},
"desktop": {
"openSettings": {
"desc": "Ouvrir la page des paramètres de l'application",
"title": "Paramètres de l'application"
},
"showApp": {
"desc": "Afficher ou masquer la fenêtre principale via un raccourci global",
"title": "Afficher/Masquer la fenêtre principale"
}
},
"editMessage": {
"desc": "Entrez en mode édition en maintenant la touche Alt enfoncée et en double-cliquant sur le message",
"title": "Éditer le message"
@@ -19,10 +29,6 @@
"desc": "Voir les instructions d'utilisation de tous les raccourcis",
"title": "Ouvrir l'aide des raccourcis"
},
"openSettings": {
"desc": "Ouvrir la page des paramètres de l'application",
"title": "Paramètres de l'application"
},
"regenerateMessage": {
"desc": "Régénérer le dernier message",
"title": "Régénérer le message"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash est le modèle le plus rentable de Google, offrant des fonctionnalités complètes."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite est le modèle le plus petit et le plus rentable de Google, conçu pour une utilisation à grande échelle."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview est le modèle le plus compact et rentable de Google, conçu pour une utilisation à grande échelle."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "Le modèle v0-1.5-md convient aux tâches quotidiennes et à la génération d'interfaces utilisateur (UI)"
},
"wanx2.1-t2i-turbo": {
"description": "Modèle de génération d'images par texte de Tongyi d'Aliyun"
},
"whisper-1": {
"description": "Modèle universel de reconnaissance vocale, prenant en charge la reconnaissance vocale multilingue, la traduction vocale et la reconnaissance de langue."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "Conflit avec les raccourcis existants",
"errors": {
"CONFLICT": "Conflit de raccourci : ce raccourci est déjà utilisé par une autre fonction",
"INVALID_FORMAT": "Format de raccourci invalide : veuillez utiliser le format correct (par exemple CommandOrControl+E)",
"INVALID_ID": "ID de raccourci invalide",
"NO_MODIFIER": "Le raccourci doit inclure une touche modificateur (Ctrl, Alt, Shift, etc.)",
"SYSTEM_OCCUPIED": "Le raccourci est déjà utilisé par le système ou une autre application",
"UNKNOWN": "Échec de la mise à jour : erreur inconnue"
},
"group": {
"conversation": "Conversation",
"desktop": "Bureau",
"essential": "Essentiel"
},
"invalidCombination": "Le raccourci doit contenir au moins une touche de modification (Ctrl, Alt, Shift) et une touche normale",
"record": "Appuyez sur une touche pour enregistrer le raccourci",
"reset": "Réinitialiser aux raccourcis par défaut",
"title": "Raccourcis clavier"
"title": "Raccourcis clavier",
"updateError": "Échec de la mise à jour du raccourci : erreur réseau ou système",
"updateSuccess": "Mise à jour du raccourci réussie"
},
"llm": {
"aesGcm": "Votre clé, votre adresse de proxy, etc. seront cryptées à l'aide de l'algorithme de chiffrement <1>AES-GCM</1>",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Generato automaticamente",
"copy": "Copia",
"copyError": "Copia non riuscita",
"copySuccess": "Chiave API copiata negli appunti",
"enterPlaceholder": "Inserisci",
"hide": "Nascondi",
"neverExpires": "Non scade mai",
"neverUsed": "Mai usato",
"show": "Mostra"
},
"form": {
"fields": {
"expiresAt": {
"label": "Data di scadenza",
"placeholder": "Non scade mai"
},
"name": {
"label": "Nome",
"placeholder": "Inserisci il nome della Chiave API"
}
},
"submit": "Crea",
"title": "Crea Chiave API"
},
"list": {
"actions": {
"create": "Crea Chiave API",
"delete": "Elimina",
"deleteConfirm": {
"actions": {
"cancel": "Annulla",
"ok": "Conferma"
},
"content": "Sei sicuro di voler eliminare questa Chiave API?",
"title": "Conferma azione"
}
},
"columns": {
"actions": "Azioni",
"expiresAt": "Data di scadenza",
"key": "Chiave",
"lastUsedAt": "Ultimo utilizzo",
"name": "Nome",
"status": "Stato attivo"
},
"title": "Elenco Chiavi API"
},
"validation": {
"required": "Il contenuto non può essere vuoto"
}
},
"date": {
"prevMonth": "Mese Scorso",
"recent30Days": "Ultimi 30 Giorni"
@@ -89,6 +142,7 @@
"words": "Parole"
},
"tab": {
"apikey": "Gestione Chiavi API",
"profile": "Profilo",
"security": "Sicurezza",
"stats": "Statistiche"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Cancella i messaggi e i file caricati della conversazione attuale",
"title": "Cancella messaggi della conversazione"
},
"desktop": {
"openSettings": {
"desc": "Apri la pagina delle impostazioni dell'app",
"title": "Impostazioni dell'app"
},
"showApp": {
"desc": "Mostra o nascondi la finestra principale con una scorciatoia globale",
"title": "Mostra/Nascondi finestra principale"
}
},
"editMessage": {
"desc": "Entra in modalità di modifica tenendo premuto Alt e facendo doppio clic sul messaggio",
"title": "Modifica messaggio"
@@ -19,10 +29,6 @@
"desc": "Visualizza le istruzioni per l'uso di tutte le scorciatoie da tastiera",
"title": "Apri aiuto scorciatoie"
},
"openSettings": {
"desc": "Apri la pagina delle impostazioni dell'app",
"title": "Impostazioni dell'app"
},
"regenerateMessage": {
"desc": "Rigenera l'ultimo messaggio",
"title": "Rigenera messaggio"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash è il modello Google con il miglior rapporto qualità-prezzo, offrendo funzionalità complete."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite è il modello più piccolo e conveniente di Google, progettato per un utilizzo su larga scala."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview è il modello Google più piccolo e con il miglior rapporto qualità-prezzo, progettato per un utilizzo su larga scala."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "Il modello v0-1.5-md è adatto per compiti quotidiani e generazione di interfacce utente (UI)"
},
"wanx2.1-t2i-turbo": {
"description": "Modello di generazione di immagini basato su testo di Tongyi di Alibaba Cloud"
},
"whisper-1": {
"description": "Modello universale di riconoscimento vocale, supporta riconoscimento vocale multilingue, traduzione vocale e identificazione della lingua."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "In conflitto con i tasti di scelta rapida esistenti",
"errors": {
"CONFLICT": "Conflitto di tasti rapidi: questo tasto è già assegnato ad un'altra funzione",
"INVALID_FORMAT": "Formato del tasto rapido non valido: utilizzare un formato corretto (es. CommandOrControl+E)",
"INVALID_ID": "ID del tasto rapido non valido",
"NO_MODIFIER": "Il tasto rapido deve includere un modificatore (Ctrl, Alt, Shift, ecc.)",
"SYSTEM_OCCUPIED": "Il tasto rapido è già occupato dal sistema o da un'altra applicazione",
"UNKNOWN": "Aggiornamento fallito: errore sconosciuto"
},
"group": {
"conversation": "Conversazione",
"desktop": "Desktop",
"essential": "Essenziale"
},
"invalidCombination": "La combinazione di tasti deve contenere almeno un tasto modificatore (Ctrl, Alt, Shift) e un tasto normale",
"record": "Premi un tasto per registrare la scorciatoia",
"reset": "Ripristina le scorciatoie predefinite",
"title": "Scorciatoie"
"title": "Scorciatoie",
"updateError": "Aggiornamento del tasto rapido fallito: errore di rete o di sistema",
"updateSuccess": "Aggiornamento del tasto rapido riuscito"
},
"llm": {
"aesGcm": "La tua chiave e l'indirizzo dell'agente saranno crittografati utilizzando l'algoritmo di crittografia <1>AES-GCM</1>",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "自動生成",
"copy": "コピー",
"copyError": "コピーに失敗しました",
"copySuccess": "APIキーがクリップボードにコピーされました",
"enterPlaceholder": "入力してください",
"hide": "非表示",
"neverExpires": "期限なし",
"neverUsed": "未使用",
"show": "表示"
},
"form": {
"fields": {
"expiresAt": {
"label": "有効期限",
"placeholder": "期限なし"
},
"name": {
"label": "名前",
"placeholder": "APIキーの名前を入力してください"
}
},
"submit": "作成",
"title": "APIキーを作成"
},
"list": {
"actions": {
"create": "APIキーを作成",
"delete": "削除",
"deleteConfirm": {
"actions": {
"cancel": "キャンセル",
"ok": "確認"
},
"content": "このAPIキーを削除してもよろしいですか?",
"title": "操作の確認"
}
},
"columns": {
"actions": "操作",
"expiresAt": "有効期限",
"key": "キー",
"lastUsedAt": "最終使用日時",
"name": "名前",
"status": "有効状態"
},
"title": "APIキー一覧"
},
"validation": {
"required": "内容を入力してください"
}
},
"date": {
"prevMonth": "先月",
"recent30Days": "過去30日間"
@@ -89,6 +142,7 @@
"words": "単語"
},
"tab": {
"apikey": "APIキー管理",
"profile": "プロフィール",
"security": "セキュリティ",
"stats": "統計"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "現在のセッションのメッセージとアップロードされたファイルをクリアする",
"title": "セッションメッセージをクリア"
},
"desktop": {
"openSettings": {
"desc": "アプリ設定ページを開く",
"title": "アプリ設定"
},
"showApp": {
"desc": "グローバルショートカットでメインウィンドウを表示または非表示にする",
"title": "メインウィンドウの表示/非表示"
}
},
"editMessage": {
"desc": "Altキーを押しながらメッセージをダブルクリックして編集モードに入ります",
"title": "メッセージを編集"
@@ -19,10 +29,6 @@
"desc": "すべてのショートカットキーの使用説明を表示する",
"title": "ショートカットヘルプを開く"
},
"openSettings": {
"desc": "アプリの設定ページを開く",
"title": "アプリ設定"
},
"regenerateMessage": {
"desc": "最後のメッセージを再生成します",
"title": "メッセージを再生成"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 FlashはGoogleのコストパフォーマンスに優れたモデルで、包括的な機能を提供します。"
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite は、Google の中で最も小さく、コストパフォーマンスに優れたモデルであり、大規模な利用を目的に設計されています。"
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite PreviewはGoogleの最小かつコストパフォーマンスに優れたモデルで、大規模利用を目的に設計されています。"
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "v0-1.5-md モデルは、日常的なタスクやユーザーインターフェース(UI)生成に適しています"
},
"wanx2.1-t2i-turbo": {
"description": "アリババクラウドのTongyiが提供するテキストから画像生成モデル"
},
"whisper-1": {
"description": "汎用音声認識モデルで、多言語の音声認識、音声翻訳、言語識別をサポートします。"
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "既存のショートカットキーと衝突しています",
"errors": {
"CONFLICT": "ホットキーの競合:このホットキーは他の機能で既に使用されています",
"INVALID_FORMAT": "ホットキーの形式が無効です:正しい形式を使用してください(例:CommandOrControl+E",
"INVALID_ID": "無効なホットキーIDです",
"NO_MODIFIER": "ホットキーには修飾キー(Ctrl、Alt、Shiftなど)が含まれている必要があります",
"SYSTEM_OCCUPIED": "ホットキーはシステムまたは他のアプリケーションで使用されています",
"UNKNOWN": "更新に失敗しました:不明なエラー"
},
"group": {
"conversation": "会話",
"desktop": "デスクトップ",
"essential": "基本"
},
"invalidCombination": "ショートカットキーには少なくとも1つの修飾キー(Ctrl、Alt、Shift)と1つの通常のキーが必要です",
"record": "ショートカットキーを録音するにはキーを押してください",
"reset": "デフォルトのショートカットキーにリセット",
"title": "ショートカットキー"
"title": "ショートカットキー",
"updateError": "ホットキーの更新に失敗しました:ネットワークまたはシステムエラー",
"updateSuccess": "ホットキーが正常に更新されました"
},
"llm": {
"aesGcm": "キーとプロキシアドレスなどは <1>AES-GCM</1> 暗号化アルゴリズムを使用して暗号化されます",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "자동 생성",
"copy": "복사",
"copyError": "복사 실패",
"copySuccess": "API 키가 클립보드에 복사되었습니다",
"enterPlaceholder": "입력하세요",
"hide": "숨기기",
"neverExpires": "만료되지 않음",
"neverUsed": "한 번도 사용되지 않음",
"show": "표시"
},
"form": {
"fields": {
"expiresAt": {
"label": "만료 시간",
"placeholder": "만료되지 않음"
},
"name": {
"label": "이름",
"placeholder": "API 키 이름을 입력하세요"
}
},
"submit": "생성",
"title": "API 키 생성"
},
"list": {
"actions": {
"create": "API 키 생성",
"delete": "삭제",
"deleteConfirm": {
"actions": {
"cancel": "취소",
"ok": "확인"
},
"content": "이 API 키를 삭제하시겠습니까?",
"title": "작업 확인"
}
},
"columns": {
"actions": "작업",
"expiresAt": "만료 시간",
"key": "키",
"lastUsedAt": "마지막 사용 시간",
"name": "이름",
"status": "활성 상태"
},
"title": "API 키 목록"
},
"validation": {
"required": "내용을 비워둘 수 없습니다"
}
},
"date": {
"prevMonth": "지난 달",
"recent30Days": "최근 30일"
@@ -89,6 +142,7 @@
"words": "단어"
},
"tab": {
"apikey": "API 키 관리",
"profile": "프로필",
"security": "보안",
"stats": "통계"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "현재 대화의 메시지와 업로드된 파일을 지웁니다",
"title": "대화 메시지 지우기"
},
"desktop": {
"openSettings": {
"desc": "애플리케이션 설정 페이지 열기",
"title": "애플리케이션 설정"
},
"showApp": {
"desc": "글로벌 단축키로 메인 창 표시 또는 숨기기",
"title": "메인 창 표시/숨기기"
}
},
"editMessage": {
"desc": "Alt 키를 누른 채로 메시지를 더블 클릭하여 편집 모드로 들어갑니다",
"title": "메시지 편집"
@@ -19,10 +29,6 @@
"desc": "모든 단축키 사용 설명을 확인합니다.",
"title": "단축키 도움말 열기"
},
"openSettings": {
"desc": "앱 설정 페이지 열기",
"title": "앱 설정"
},
"regenerateMessage": {
"desc": "마지막 메시지를 다시 생성합니다",
"title": "메시지 다시 생성"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash는 구글에서 가장 가성비가 뛰어난 모델로, 포괄적인 기능을 제공합니다."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite는 Google의 가장 작고 가성비가 뛰어난 모델로, 대규모 사용을 위해 설계되었습니다."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview는 구글의 가장 작고 가성비가 뛰어난 모델로, 대규모 사용을 위해 설계되었습니다."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "v0-1.5-md 모델은 일상 작업 및 사용자 인터페이스(UI) 생성에 적합합니다"
},
"wanx2.1-t2i-turbo": {
"description": "알리클라우드 통의(通义) 산하의 텍스트-이미지 생성 모델"
},
"whisper-1": {
"description": "범용 음성 인식 모델로, 다국어 음성 인식, 음성 번역 및 언어 인식을 지원합니다."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "기존 단축키와 충돌",
"errors": {
"CONFLICT": "단축키 충돌: 해당 단축키는 이미 다른 기능에서 사용 중입니다",
"INVALID_FORMAT": "단축키 형식이 올바르지 않습니다: 올바른 형식(예: CommandOrControl+E)을 사용하세요",
"INVALID_ID": "유효하지 않은 단축키 ID입니다",
"NO_MODIFIER": "단축키에는 반드시 수정 키(Ctrl, Alt, Shift 등)가 포함되어야 합니다",
"SYSTEM_OCCUPIED": "단축키가 시스템 또는 다른 애플리케이션에서 사용 중입니다",
"UNKNOWN": "업데이트 실패: 알 수 없는 오류"
},
"group": {
"conversation": "대화",
"desktop": "데스크톱",
"essential": "기본"
},
"invalidCombination": "단축키는 최소한 하나의 수정 키(Ctrl, Alt, Shift)와 하나의 일반 키를 포함해야 합니다",
"record": "단축키를 녹음하려면 키를 누르세요",
"reset": "기본 단축키로 재설정",
"title": "단축키"
"title": "단축키",
"updateError": "단축키 업데이트 실패: 네트워크 또는 시스템 오류",
"updateSuccess": "단축키가 성공적으로 업데이트되었습니다"
},
"llm": {
"aesGcm": "귀하의 키 및 프록시 주소는 <1>AES-GCM</1> 암호화 알고리즘을 사용하여 암호화됩니다",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Automatisch gegenereerd",
"copy": "Kopiëren",
"copyError": "Kopiëren mislukt",
"copySuccess": "API-sleutel is gekopieerd naar het klembord",
"enterPlaceholder": "Voer in",
"hide": "Verbergen",
"neverExpires": "Verloopt nooit",
"neverUsed": "Nooit gebruikt",
"show": "Weergeven"
},
"form": {
"fields": {
"expiresAt": {
"label": "Vervaldatum",
"placeholder": "Verloopt nooit"
},
"name": {
"label": "Naam",
"placeholder": "Voer de naam van de API-sleutel in"
}
},
"submit": "Aanmaken",
"title": "API-sleutel aanmaken"
},
"list": {
"actions": {
"create": "API-sleutel aanmaken",
"delete": "Verwijderen",
"deleteConfirm": {
"actions": {
"cancel": "Annuleren",
"ok": "Bevestigen"
},
"content": "Weet u zeker dat u deze API-sleutel wilt verwijderen?",
"title": "Bevestig actie"
}
},
"columns": {
"actions": "Acties",
"expiresAt": "Vervaldatum",
"key": "Sleutel",
"lastUsedAt": "Laatst gebruikt",
"name": "Naam",
"status": "Status"
},
"title": "API-sleutellijst"
},
"validation": {
"required": "Inhoud mag niet leeg zijn"
}
},
"date": {
"prevMonth": "Vorige maand",
"recent30Days": "Laatste 30 dagen"
@@ -89,6 +142,7 @@
"words": "Woorden"
},
"tab": {
"apikey": "API-sleutelbeheer",
"profile": "Profiel",
"security": "Beveiliging",
"stats": "Statistieken"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Verwijder de berichten en geüploade bestanden van de huidige sessie",
"title": "Verwijder sessieberichten"
},
"desktop": {
"openSettings": {
"desc": "Open de applicatie-instellingenpagina",
"title": "Applicatie-instellingen"
},
"showApp": {
"desc": "Toon of verberg het hoofdvenster met een globale sneltoets",
"title": "Toon/verberg hoofdvenster"
}
},
"editMessage": {
"desc": "Ga naar de bewerkingsmodus door Alt ingedrukt te houden en op het bericht te dubbelklikken",
"title": "Bewerk bericht"
@@ -19,10 +29,6 @@
"desc": "Bekijk de gebruiksaanwijzing voor alle sneltoetsen",
"title": "Open sneltoets hulp"
},
"openSettings": {
"desc": "Open de applicatie-instellingenpagina",
"title": "Applicatie-instellingen"
},
"regenerateMessage": {
"desc": "Genereer het laatste bericht opnieuw",
"title": "Genereer bericht opnieuw"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash is het meest kosteneffectieve model van Google en biedt uitgebreide functionaliteiten."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite is het kleinste en meest kosteneffectieve model van Google, speciaal ontworpen voor grootschalig gebruik."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview is het kleinste en meest kosteneffectieve model van Google, speciaal ontworpen voor grootschalig gebruik."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "Het v0-1.5-md model is geschikt voor dagelijkse taken en het genereren van gebruikersinterfaces (UI)"
},
"wanx2.1-t2i-turbo": {
"description": "Tekst-naar-beeldmodel van Alibaba Cloud Tongyi"
},
"whisper-1": {
"description": "Algemeen spraakherkenningsmodel, ondersteunt meertalige spraakherkenning, spraakvertaling en taalherkenning."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "Conflict met bestaande sneltoetsen",
"errors": {
"CONFLICT": "Sneltoetsconflict: deze sneltoets wordt al door een andere functie gebruikt",
"INVALID_FORMAT": "Ongeldig sneltoetsformaat: gebruik het juiste formaat (bijv. CommandOrControl+E)",
"INVALID_ID": "Ongeldige sneltoets-ID",
"NO_MODIFIER": "Sneltoets moet een modificatortoets bevatten (Ctrl, Alt, Shift, enz.)",
"SYSTEM_OCCUPIED": "Sneltoets wordt al door het systeem of een andere applicatie gebruikt",
"UNKNOWN": "Bijwerken mislukt: onbekende fout"
},
"group": {
"conversation": "Gesprek",
"desktop": "Desktop",
"essential": "Essentieel"
},
"invalidCombination": "Sneltoets moet ten minste één modifier-toets (Ctrl, Alt, Shift) en één reguliere toets bevatten",
"record": "Druk op een toets om de sneltoets op te nemen",
"reset": "Reset naar standaard sneltoetsen",
"title": "Sneltoetsen"
"title": "Sneltoetsen",
"updateError": "Sneltoets bijwerken mislukt: netwerk- of systeemfout",
"updateSuccess": "Sneltoets succesvol bijgewerkt"
},
"llm": {
"aesGcm": "Uw sleutel en proxy-adres zullen worden versleuteld met het <1>AES-GCM</1> encryptie-algoritme",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Automatycznie wygenerowany",
"copy": "Kopiuj",
"copyError": "Kopiowanie nie powiodło się",
"copySuccess": "Klucz API został skopiowany do schowka",
"enterPlaceholder": "Wpisz",
"hide": "Ukryj",
"neverExpires": "Nigdy nie wygasa",
"neverUsed": "Nigdy nie używany",
"show": "Pokaż"
},
"form": {
"fields": {
"expiresAt": {
"label": "Data wygaśnięcia",
"placeholder": "Nigdy nie wygasa"
},
"name": {
"label": "Nazwa",
"placeholder": "Wpisz nazwę klucza API"
}
},
"submit": "Utwórz",
"title": "Utwórz klucz API"
},
"list": {
"actions": {
"create": "Utwórz klucz API",
"delete": "Usuń",
"deleteConfirm": {
"actions": {
"cancel": "Anuluj",
"ok": "Potwierdź"
},
"content": "Czy na pewno chcesz usunąć ten klucz API?",
"title": "Potwierdź operację"
}
},
"columns": {
"actions": "Akcje",
"expiresAt": "Data wygaśnięcia",
"key": "Klucz",
"lastUsedAt": "Ostatnie użycie",
"name": "Nazwa",
"status": "Status aktywacji"
},
"title": "Lista kluczy API"
},
"validation": {
"required": "Pole nie może być puste"
}
},
"date": {
"prevMonth": "Poprzedni miesiąc",
"recent30Days": "Ostatnie 30 dni"
@@ -89,6 +142,7 @@
"words": "Słowa"
},
"tab": {
"apikey": "Zarządzanie kluczami API",
"profile": "Profil",
"security": "Bezpieczeństwo",
"stats": "Statystyki"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Wyczyść wiadomości i przesłane pliki w bieżącej rozmowie",
"title": "Wyczyść wiadomości rozmowy"
},
"desktop": {
"openSettings": {
"desc": "Otwórz stronę ustawień aplikacji",
"title": "Ustawienia aplikacji"
},
"showApp": {
"desc": "Globalny skrót klawiszowy do wyświetlania lub ukrywania głównego okna",
"title": "Pokaż/Ukryj główne okno"
}
},
"editMessage": {
"desc": "Wejdź w tryb edycji, przytrzymując klawisz Alt i podwójnie klikając wiadomość",
"title": "Edytuj wiadomość"
@@ -19,10 +29,6 @@
"desc": "Zobacz wszystkie instrukcje dotyczące skrótów klawiszowych",
"title": "Otwórz pomoc dotyczącą skrótów klawiszowych"
},
"openSettings": {
"desc": "Otwórz stronę ustawień aplikacji",
"title": "Ustawienia aplikacji"
},
"regenerateMessage": {
"desc": "Ponownie wygeneruj ostatnią wiadomość",
"title": "Ponownie wygeneruj wiadomość"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash to najbardziej opłacalny model Google, oferujący wszechstronne funkcje."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite to najmniejszy i najbardziej opłacalny model Google, zaprojektowany z myślą o szerokim zastosowaniu."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview to najmniejszy i najbardziej opłacalny model Google, zaprojektowany z myślą o masowym zastosowaniu."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "Model v0-1.5-md jest odpowiedni do codziennych zadań i generowania interfejsu użytkownika (UI)"
},
"wanx2.1-t2i-turbo": {
"description": "Model generowania obrazów firmy Alibaba Cloud Tongyi"
},
"whisper-1": {
"description": "Uniwersalny model rozpoznawania mowy, obsługujący wielojęzyczne rozpoznawanie mowy, tłumaczenie mowy oraz identyfikację języka."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "Kolizja z istniejącymi skrótami klawiszowymi",
"errors": {
"CONFLICT": "Konflikt skrótu klawiszowego: ten skrót jest już używany przez inną funkcję",
"INVALID_FORMAT": "Nieprawidłowy format skrótu klawiszowego: użyj poprawnego formatu (np. CommandOrControl+E)",
"INVALID_ID": "Nieprawidłowy identyfikator skrótu klawiszowego",
"NO_MODIFIER": "Skrót klawiszowy musi zawierać klawisz modyfikujący (Ctrl, Alt, Shift itp.)",
"SYSTEM_OCCUPIED": "Skrót klawiszowy jest zajęty przez system lub inną aplikację",
"UNKNOWN": "Aktualizacja nie powiodła się: nieznany błąd"
},
"group": {
"conversation": "Rozmowa",
"desktop": "Pulpit",
"essential": "Podstawowy"
},
"invalidCombination": "Skrót klawiszowy musi zawierać przynajmniej jeden klawisz modyfikujący (Ctrl, Alt, Shift) oraz jeden klawisz zwykły",
"record": "Naciśnij klawisz, aby nagrać skrót klawiszowy",
"reset": "Przywróć domyślne skróty klawiszowe",
"title": "Skróty klawiszowe"
"title": "Skróty klawiszowe",
"updateError": "Aktualizacja skrótu klawiszowego nie powiodła się: błąd sieci lub systemu",
"updateSuccess": "Skrót klawiszowy został pomyślnie zaktualizowany"
},
"llm": {
"aesGcm": "Twój klucz, adres proxy i inne będą szyfrowane za pomocą algorytmu szyfrowania <1>AES-GCM</1>",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Gerado automaticamente",
"copy": "Copiar",
"copyError": "Falha ao copiar",
"copySuccess": "Chave API copiada para a área de transferência",
"enterPlaceholder": "Por favor, insira",
"hide": "Ocultar",
"neverExpires": "Nunca expira",
"neverUsed": "Nunca usado",
"show": "Mostrar"
},
"form": {
"fields": {
"expiresAt": {
"label": "Data de expiração",
"placeholder": "Nunca expira"
},
"name": {
"label": "Nome",
"placeholder": "Por favor, insira o nome da Chave API"
}
},
"submit": "Criar",
"title": "Criar Chave API"
},
"list": {
"actions": {
"create": "Criar Chave API",
"delete": "Excluir",
"deleteConfirm": {
"actions": {
"cancel": "Cancelar",
"ok": "Confirmar"
},
"content": "Tem certeza de que deseja excluir esta Chave API?",
"title": "Confirmar ação"
}
},
"columns": {
"actions": "Ações",
"expiresAt": "Data de expiração",
"key": "Chave",
"lastUsedAt": "Último uso",
"name": "Nome",
"status": "Status de ativação"
},
"title": "Lista de Chaves API"
},
"validation": {
"required": "O conteúdo não pode estar vazio"
}
},
"date": {
"prevMonth": "Último Mês",
"recent30Days": "Últimos 30 Dias"
@@ -89,6 +142,7 @@
"words": "Palavras"
},
"tab": {
"apikey": "Gerenciamento de Chave API",
"profile": "Perfil",
"security": "Segurança",
"stats": "Estatísticas"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Limpar as mensagens da conversa atual e os arquivos enviados",
"title": "Limpar mensagens da conversa"
},
"desktop": {
"openSettings": {
"desc": "Abrir a página de configurações do aplicativo",
"title": "Configurações do Aplicativo"
},
"showApp": {
"desc": "Atalho global para mostrar ou ocultar a janela principal",
"title": "Mostrar/Ocultar Janela Principal"
}
},
"editMessage": {
"desc": "Entre no modo de edição pressionando Alt e clicando duas vezes na mensagem",
"title": "Editar mensagem"
@@ -19,10 +29,6 @@
"desc": "Ver as instruções de uso de todos os atalhos",
"title": "Abrir ajuda de atalhos"
},
"openSettings": {
"desc": "Abra a página de configurações do aplicativo",
"title": "Configurações do Aplicativo"
},
"regenerateMessage": {
"desc": "Regenerar a última mensagem",
"title": "Regenerar mensagem"
+6
View File
@@ -1100,6 +1100,9 @@
"gemini-2.5-flash": {
"description": "Gemini 2.5 Flash é o modelo com melhor custo-benefício do Google, oferecendo funcionalidades abrangentes."
},
"gemini-2.5-flash-lite": {
"description": "Gemini 2.5 Flash-Lite é o modelo mais compacto e com melhor custo-benefício do Google, projetado para uso em larga escala."
},
"gemini-2.5-flash-lite-preview-06-17": {
"description": "Gemini 2.5 Flash-Lite Preview é o modelo mais compacto e com melhor custo-benefício do Google, projetado para uso em larga escala."
},
@@ -2429,6 +2432,9 @@
"v0-1.5-md": {
"description": "O modelo v0-1.5-md é adequado para tarefas diárias e geração de interfaces de usuário (UI)"
},
"wanx2.1-t2i-turbo": {
"description": "Modelo de geração de imagens da Alibaba Cloud Tongyi"
},
"whisper-1": {
"description": "Modelo universal de reconhecimento de voz, suportando reconhecimento de voz multilíngue, tradução de voz e identificação de idioma."
},
+12 -1
View File
@@ -45,14 +45,25 @@
},
"hotkey": {
"conflicts": "Conflito com teclas de atalho existentes",
"errors": {
"CONFLICT": "Conflito de atalho: este atalho já está em uso por outra função",
"INVALID_FORMAT": "Formato de atalho inválido: por favor, use o formato correto (ex: CommandOrControl+E)",
"INVALID_ID": "ID de atalho inválido",
"NO_MODIFIER": "O atalho deve incluir uma tecla modificadora (Ctrl, Alt, Shift, etc.)",
"SYSTEM_OCCUPIED": "Atalho já está ocupado pelo sistema ou por outro aplicativo",
"UNKNOWN": "Falha na atualização: erro desconhecido"
},
"group": {
"conversation": "Conversa",
"desktop": "Área de trabalho",
"essential": "Essencial"
},
"invalidCombination": "A combinação de teclas de atalho deve incluir pelo menos uma tecla modificadora (Ctrl, Alt, Shift) e uma tecla comum",
"record": "Pressione a tecla para gravar o atalho",
"reset": "Redefinir para os atalhos padrão",
"title": "Atalhos"
"title": "Atalhos",
"updateError": "Falha na atualização do atalho: erro de rede ou sistema",
"updateSuccess": "Atalho atualizado com sucesso"
},
"llm": {
"aesGcm": "Suas chaves, endereço do agente, etc., serão criptografados usando o algoritmo de criptografia <1>AES-GCM</1>",
+54
View File
@@ -1,4 +1,57 @@
{
"apikey": {
"display": {
"autoGenerated": "Автоматически сгенерировано",
"copy": "Копировать",
"copyError": "Ошибка копирования",
"copySuccess": "API ключ скопирован в буфер обмена",
"enterPlaceholder": "Пожалуйста, введите",
"hide": "Скрыть",
"neverExpires": "Никогда не истекает",
"neverUsed": "Никогда не использовался",
"show": "Показать"
},
"form": {
"fields": {
"expiresAt": {
"label": "Срок действия",
"placeholder": "Никогда не истекает"
},
"name": {
"label": "Название",
"placeholder": "Пожалуйста, введите название API ключа"
}
},
"submit": "Создать",
"title": "Создать API ключ"
},
"list": {
"actions": {
"create": "Создать API ключ",
"delete": "Удалить",
"deleteConfirm": {
"actions": {
"cancel": "Отмена",
"ok": "Подтвердить"
},
"content": "Вы уверены, что хотите удалить этот API ключ?",
"title": "Подтверждение действия"
}
},
"columns": {
"actions": "Действия",
"expiresAt": "Срок действия",
"key": "Ключ",
"lastUsedAt": "Последнее использование",
"name": "Название",
"status": "Статус активации"
},
"title": "Список API ключей"
},
"validation": {
"required": "Поле не может быть пустым"
}
},
"date": {
"prevMonth": "Прошлый месяц",
"recent30Days": "Последние 30 дней"
@@ -89,6 +142,7 @@
"words": "Слова"
},
"tab": {
"apikey": "Управление API ключами",
"profile": "Профиль",
"security": "Безопасность",
"stats": "Статистика"
+10 -4
View File
@@ -7,6 +7,16 @@
"desc": "Очистить сообщения текущего сеанса и загруженные файлы",
"title": "Очистить сообщения сеанса"
},
"desktop": {
"openSettings": {
"desc": "Открыть страницу настроек приложения",
"title": "Настройки приложения"
},
"showApp": {
"desc": "Глобальная горячая клавиша для отображения или скрытия главного окна",
"title": "Показать/Скрыть главное окно"
}
},
"editMessage": {
"desc": "Войти в режим редактирования, удерживая Alt и дважды щелкнув по сообщению",
"title": "Редактировать сообщение"
@@ -19,10 +29,6 @@
"desc": "Просмотреть инструкции по использованию всех горячих клавиш",
"title": "Открыть справку по горячим клавишам"
},
"openSettings": {
"desc": "Открыть страницу настроек приложения",
"title": "Настройки приложения"
},
"regenerateMessage": {
"desc": "Сгенерировать последнее сообщение заново",
"title": "Перегенерировать сообщение"

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