Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fa6d64df2 | |||
| 403aebd52e | |||
| 356cf0c392 | |||
| be98d56ef4 | |||
| 3fae1b2638 | |||
| ee4cc6c2e0 | |||
| b8c0e2d639 | |||
| 74d20bdbe8 | |||
| 7bab44e74c | |||
| fdaa72564c | |||
| 7d85151cb6 | |||
| 23ef2eea59 | |||
| 74ab822140 | |||
| d726ff108d | |||
| dd7b661140 | |||
| 49023419cf | |||
| 2eccbc79eb | |||
| 3527cb65f1 | |||
| 1731c841d8 | |||
| 404ac21229 | |||
| 2eaa2dbea0 | |||
| 50c0ed168d | |||
| 780e231afa | |||
| 07f3e6a4c4 | |||
| 3c35edced5 | |||
| 15770f188f | |||
| 9d7c6014fd | |||
| d1e4a54b01 | |||
| 1bc8815fb4 | |||
| 284826bed0 | |||
| b50f1212cb | |||
| 946517a52e | |||
| d30cc62acf | |||
| 0192140909 | |||
| 9bae13b6c1 | |||
| fd7662c3ac | |||
| 66dbb246d9 | |||
| 3a2935a6e3 | |||
| 82ca0074d4 | |||
| 225d3b6ed5 | |||
| 66984d9418 | |||
| b16f19bfed | |||
| f61b222e9e | |||
| 368f7dbbc1 | |||
| 9c2350e643 | |||
| 7e8e5ef5b2 | |||
| 0ed9e7d947 |
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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>
|
||||
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 171 KiB |
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,4 +31,5 @@ export const STORE_DEFAULTS: ElectronMainStore = {
|
||||
networkProxy: defaultProxySettings,
|
||||
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
|
||||
storagePath: appStorageDir,
|
||||
themeMode: 'auto',
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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+,',
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface ElectronMainStore {
|
||||
networkProxy: NetworkProxySettings;
|
||||
shortcuts: Record<string, string>;
|
||||
storagePath: string;
|
||||
themeMode: 'dark' | 'light' | 'auto';
|
||||
}
|
||||
|
||||
export type StoreKey = keyof ElectronMainStore;
|
||||
|
||||
@@ -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` 进行错误封装,保持错误信息的一致性
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
|
||||
@@ -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": "الإحصائيات"
|
||||
|
||||
@@ -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": "إعادة توليد الرسالة"
|
||||
|
||||
@@ -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": "نموذج التعرف على الصوت العام، يدعم التعرف على الصوت بعدة لغات، الترجمة الصوتية، والتعرف على اللغة."
|
||||
},
|
||||
|
||||
@@ -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> لتشفير مفتاحك وعنوان الوكيل",
|
||||
|
||||
@@ -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": "Статистика"
|
||||
|
||||
@@ -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": "Прегенериране на съобщение"
|
||||
|
||||
@@ -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": "Универсален модел за разпознаване на реч, поддържащ многоезично разпознаване на реч, превод на реч и разпознаване на език."
|
||||
},
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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": "آمار"
|
||||
|
||||
@@ -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": "تولید مجدد پیام"
|
||||
|
||||
@@ -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": "مدل شناسایی گفتار عمومی که از شناسایی گفتار چندزبانه، ترجمه گفتار و شناسایی زبان پشتیبانی میکند."
|
||||
},
|
||||
|
||||
@@ -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> رمزگذاری خواهد شد",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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": "統計"
|
||||
|
||||
@@ -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": "メッセージを再生成"
|
||||
|
||||
@@ -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": "汎用音声認識モデルで、多言語の音声認識、音声翻訳、言語識別をサポートします。"
|
||||
},
|
||||
|
||||
@@ -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> 暗号化アルゴリズムを使用して暗号化されます",
|
||||
|
||||
@@ -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": "통계"
|
||||
|
||||
@@ -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": "메시지 다시 생성"
|
||||
|
||||
@@ -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": "범용 음성 인식 모델로, 다국어 음성 인식, 음성 번역 및 언어 인식을 지원합니다."
|
||||
},
|
||||
|
||||
@@ -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> 암호화 알고리즘을 사용하여 암호화됩니다",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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ść"
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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": "Статистика"
|
||||
|
||||
@@ -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": "Перегенерировать сообщение"
|
||||
|
||||