Compare commits
86 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 | |||
| 19cc6e8562 | |||
| 2696de4078 | |||
| 9fb4b0dfc3 | |||
| caffbbd384 | |||
| 8a89b1a14e | |||
| dd1a6356b7 | |||
| d87919eba2 | |||
| 4bcf9822f7 | |||
| 9f2eea3d2f | |||
| 4917d175bb | |||
| 5b53773dc6 | |||
| 46bdf21f37 | |||
| 60739bc903 | |||
| 75273d5497 | |||
| 22e2de2b00 | |||
| cbddf3ed25 | |||
| 14fd4bb2e7 | |||
| cef8208457 | |||
| 7d85a772db | |||
| 83dd2865f0 | |||
| c04507e34f | |||
| c801f9ce58 | |||
| ac2a83a3ce | |||
| 7bdda9bab4 | |||
| e8321355f8 | |||
| 27c4881205 | |||
| 472c40e969 | |||
| c98e7f8032 | |||
| 0b1557d6ad | |||
| 5d852be8a2 | |||
| 993b0fa81f | |||
| 83783b4650 | |||
| 7dd65f0cb4 | |||
| ecf1fdc2f7 | |||
| 71c33300cf | |||
| 768ee2bf23 | |||
| d23d8f1c29 | |||
| 7eadecd340 | |||
| 785406be9a |
@@ -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.
|
||||
|
||||
@@ -214,6 +214,176 @@ describe('<UserProfile />', () => {
|
||||
**修复方法**: 更新了测试文件中的 mock 数据结构,使其与最新的 API 响应格式保持一致。具体修改了 `user.test.ts` 中的 `mockUserData` 对象结构。
|
||||
```
|
||||
|
||||
## 🎯 测试编写最佳实践
|
||||
|
||||
### Mock 数据策略:追求"低成本的真实性" 📋
|
||||
|
||||
**核心原则**: 测试数据应默认追求真实性,只有在引入"高昂的测试成本"时才进行简化。
|
||||
|
||||
#### 什么是"高昂的测试成本"?
|
||||
|
||||
"高成本"指的是测试中引入了外部依赖,使测试变慢、不稳定或复杂:
|
||||
|
||||
- **文件 I/O 操作**:读写硬盘文件
|
||||
- **网络请求**:HTTP 调用、数据库连接
|
||||
- **系统调用**:获取系统时间、环境变量等
|
||||
|
||||
#### ✅ 推荐做法:Mock 依赖,保留真实数据
|
||||
|
||||
```typescript
|
||||
// ✅ 好的做法:Mock I/O 操作,但使用真实的文件内容格式
|
||||
describe('parseContentType', () => {
|
||||
beforeEach(() => {
|
||||
// Mock 文件读取操作(避免真实 I/O)
|
||||
vi.spyOn(fs, 'readFileSync').mockImplementation((path) => {
|
||||
// 但返回真实的文件内容格式
|
||||
if (path.includes('.pdf')) return '%PDF-1.4\n%âãÏÓ'; // 真实 PDF 文件头
|
||||
if (path.includes('.png')) return '\x89PNG\r\n\x1a\n'; // 真实 PNG 文件头
|
||||
return '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect PDF content type correctly', () => {
|
||||
const result = parseContentType('/path/to/file.pdf');
|
||||
expect(result).toBe('application/pdf');
|
||||
});
|
||||
});
|
||||
|
||||
// ❌ 过度简化:使用不真实的数据
|
||||
describe('parseContentType', () => {
|
||||
it('should detect PDF content type correctly', () => {
|
||||
// 这种简化数据没有测试价值
|
||||
const result = parseContentType('fake-pdf-content');
|
||||
expect(result).toBe('application/pdf');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 🎯 真实标识符的价值
|
||||
|
||||
```typescript
|
||||
// ✅ 使用真实的提供商标识符
|
||||
it('should parse OpenAI model list correctly', () => {
|
||||
const result = parseModelString('openai', '+gpt-4,+gpt-3.5-turbo');
|
||||
expect(result.add).toHaveLength(2);
|
||||
expect(result.add[0].id).toBe('gpt-4');
|
||||
});
|
||||
|
||||
// ❌ 使用占位符标识符(价值较低)
|
||||
it('should parse model list correctly', () => {
|
||||
const result = parseModelString('test-provider', '+model1,+model2');
|
||||
expect(result.add).toHaveLength(2);
|
||||
// 这种测试对理解真实场景帮助不大
|
||||
});
|
||||
```
|
||||
|
||||
### 错误处理测试:测试"行为"而非"文本" ⚠️
|
||||
|
||||
**核心原则**: 测试应该验证程序在错误发生时的行为是可预测的,而不是验证易变的错误信息文本。
|
||||
|
||||
#### ✅ 推荐的错误测试方式
|
||||
|
||||
```typescript
|
||||
// ✅ 测试是否抛出错误
|
||||
it('should throw error when invalid input provided', () => {
|
||||
expect(() => processInput(null)).toThrow();
|
||||
});
|
||||
|
||||
// ✅ 测试错误类型(最推荐)
|
||||
it('should throw ValidationError for invalid data', () => {
|
||||
expect(() => validateUser({})).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
// ✅ 测试错误属性而非消息文本
|
||||
it('should throw error with correct error code', () => {
|
||||
expect(() => processPayment({})).toThrow(
|
||||
expect.objectContaining({
|
||||
code: 'INVALID_PAYMENT_DATA',
|
||||
statusCode: 400,
|
||||
}),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
#### ❌ 应避免的做法
|
||||
|
||||
```typescript
|
||||
// ❌ 过度依赖具体错误信息文本
|
||||
it('should throw specific error message', () => {
|
||||
expect(() => processUser({})).toThrow('用户数据不能为空,请检查输入参数');
|
||||
// 这种测试很脆弱,错误文案稍有修改就会失败
|
||||
});
|
||||
```
|
||||
|
||||
#### 🎯 例外情况:何时可以测试错误信息
|
||||
|
||||
```typescript
|
||||
// ✅ 测试标准 API 错误(这是契约的一部分)
|
||||
it('should return proper HTTP error for API', () => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.error).toBe('Bad Request');
|
||||
});
|
||||
|
||||
// ✅ 测试错误信息的关键部分(使用正则)
|
||||
it('should include field name in validation error', () => {
|
||||
expect(() => validateField('email', '')).toThrow(/email/i);
|
||||
});
|
||||
```
|
||||
|
||||
### 疑难解答:警惕模块污染 🚨
|
||||
|
||||
**识别信号**: 当你的测试出现以下"灵异"现象时,优先怀疑模块污染:
|
||||
|
||||
- 单独运行某个测试通过,但和其他测试一起运行就失败
|
||||
- 测试的执行顺序影响结果
|
||||
- Mock 设置看起来正确,但实际使用的是旧的 Mock 版本
|
||||
|
||||
#### 典型场景:动态 Mock 同一模块
|
||||
|
||||
```typescript
|
||||
// ❌ 容易出现模块污染的写法
|
||||
describe('ConfigService', () => {
|
||||
it('should work in development mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: true }));
|
||||
const { getSettings } = await import('./configService'); // 第一次加载
|
||||
expect(getSettings().debugMode).toBe(true);
|
||||
});
|
||||
|
||||
it('should work in production mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: false }));
|
||||
const { getSettings } = await import('./configService'); // 可能使用缓存的旧版本!
|
||||
expect(getSettings().debugMode).toBe(false); // ❌ 可能失败
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 使用 resetModules 解决模块污染
|
||||
describe('ConfigService', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules(); // 清除模块缓存,确保每个测试都是干净的环境
|
||||
});
|
||||
|
||||
it('should work in development mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: true }));
|
||||
const { getSettings } = await import('./configService');
|
||||
expect(getSettings().debugMode).toBe(true);
|
||||
});
|
||||
|
||||
it('should work in production mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: false }));
|
||||
const { getSettings } = await import('./configService');
|
||||
expect(getSettings().debugMode).toBe(false); // ✅ 测试通过
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 🔧 排查和解决步骤
|
||||
|
||||
1. **识别问题**: 测试失败时,首先问自己:"是否有多个测试在 Mock 同一个模块?"
|
||||
2. **添加隔离**: 在 `beforeEach` 中添加 `vi.resetModules()`
|
||||
3. **验证修复**: 重新运行测试,确认问题解决
|
||||
|
||||
**记住**: `vi.resetModules()` 是解决测试"灵异"失败的终极武器,当常规调试方法都无效时,它往往能一针见血地解决问题。
|
||||
|
||||
## 📂 测试文件组织
|
||||
|
||||
### 文件命名约定
|
||||
@@ -320,4 +490,7 @@ git show HEAD -- path/to/component.ts | cat # 查看最新提交的修改
|
||||
- **修复原则**: 失败1-2次后寻求帮助,测试命名关注行为而非实现细节
|
||||
- **调试流程**: 复现 → 分析 → 假设 → 修复 → 验证 → 总结
|
||||
- **文件组织**: 优先在现有 `describe` 块中添加测试,避免创建冗余顶级块
|
||||
- **数据策略**: 默认追求真实性,只有高成本(I/O、网络等)时才简化
|
||||
- **错误测试**: 测试错误类型和行为,避免依赖具体的错误信息文本
|
||||
- **模块污染**: 测试"灵异"失败时,优先怀疑模块污染,使用 `vi.resetModules()` 解决
|
||||
- **安全要求**: Model 测试必须包含权限检查,并在双环境下验证通过
|
||||
|
||||
@@ -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,591 @@
|
||||
|
||||
# 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>
|
||||
|
||||
#### ♻ Code Refactoring
|
||||
|
||||
- **misc**: Add badge and improve document.
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Update tray icon.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Code refactoring
|
||||
|
||||
- **misc**: Add badge and improve document, closes [#8528](https://github.com/lobehub/lobe-chat/issues/8528) ([9fb4b0d](https://github.com/lobehub/lobe-chat/commit/9fb4b0d))
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Update tray icon, closes [#8530](https://github.com/lobehub/lobe-chat/issues/8530) ([2696de4](https://github.com/lobehub/lobe-chat/commit/2696de4))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.102.3](https://github.com/lobehub/lobe-chat/compare/v1.102.2...v1.102.3)
|
||||
|
||||
<sup>Released on **2025-07-22**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Remove debug logging from ModelRuntime and async caller.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Remove debug logging from ModelRuntime and async caller, closes [#8525](https://github.com/lobehub/lobe-chat/issues/8525) ([dd1a635](https://github.com/lobehub/lobe-chat/commit/dd1a635))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.102.2](https://github.com/lobehub/lobe-chat/compare/v1.102.1...v1.102.2)
|
||||
|
||||
<sup>Released on **2025-07-22**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Add notification for desktop.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Add notification for desktop, closes [#8523](https://github.com/lobehub/lobe-chat/issues/8523) ([4917d17](https://github.com/lobehub/lobe-chat/commit/4917d17))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.102.1](https://github.com/lobehub/lobe-chat/compare/v1.102.0...v1.102.1)
|
||||
|
||||
<sup>Released on **2025-07-21**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **groq**: Enable streaming for tool calls and add Kimi K2 model.
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Modal list header sticky style.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **groq**: Enable streaming for tool calls and add Kimi K2 model, closes [#8510](https://github.com/lobehub/lobe-chat/issues/8510) ([60739bc](https://github.com/lobehub/lobe-chat/commit/60739bc))
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Modal list header sticky style, closes [#8514](https://github.com/lobehub/lobe-chat/issues/8514) ([75273d5](https://github.com/lobehub/lobe-chat/commit/75273d5))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 1.102.0](https://github.com/lobehub/lobe-chat/compare/v1.101.2...v1.102.0)
|
||||
|
||||
<sup>Released on **2025-07-21**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **misc**: Add image generation capabilities using Google AI Imagen API.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **misc**: Add image generation capabilities using Google AI Imagen API, closes [#8503](https://github.com/lobehub/lobe-chat/issues/8503) ([cef8208](https://github.com/lobehub/lobe-chat/commit/cef8208))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.101.2](https://github.com/lobehub/lobe-chat/compare/v1.101.1...v1.101.2)
|
||||
|
||||
<sup>Released on **2025-07-21**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Fix lobehub provider `/chat` in desktop.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Fix lobehub provider `/chat` in desktop, closes [#8508](https://github.com/lobehub/lobe-chat/issues/8508) ([c801f9c](https://github.com/lobehub/lobe-chat/commit/c801f9c))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.101.1](https://github.com/lobehub/lobe-chat/compare/v1.101.0...v1.101.1)
|
||||
|
||||
<sup>Released on **2025-07-19**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Try fix authorization code exchange & pin next-auto to `beta.29`.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Try fix authorization code exchange & pin next-auto to `beta.29`, closes [#8496](https://github.com/lobehub/lobe-chat/issues/8496) ([27c4881](https://github.com/lobehub/lobe-chat/commit/27c4881))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 1.101.0](https://github.com/lobehub/lobe-chat/compare/v1.100.2...v1.101.0)
|
||||
|
||||
<sup>Released on **2025-07-19**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **misc**: Add zhipu cogview4.
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Some ai image bugs.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **misc**: Add zhipu cogview4, closes [#8486](https://github.com/lobehub/lobe-chat/issues/8486) ([0b1557d](https://github.com/lobehub/lobe-chat/commit/0b1557d))
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Some ai image bugs, closes [#8490](https://github.com/lobehub/lobe-chat/issues/8490) ([5d852be](https://github.com/lobehub/lobe-chat/commit/5d852be))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.100.2](https://github.com/lobehub/lobe-chat/compare/v1.100.1...v1.100.2)
|
||||
|
||||
<sup>Released on **2025-07-18**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Fix webapi proxy with clerk.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Fix webapi proxy with clerk, closes [#8479](https://github.com/lobehub/lobe-chat/issues/8479) ([7dd65f0](https://github.com/lobehub/lobe-chat/commit/7dd65f0))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.100.1](https://github.com/lobehub/lobe-chat/compare/v1.100.0...v1.100.1)
|
||||
|
||||
<sup>Released on **2025-07-17**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Use server env config image models.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Use server env config image models, closes [#8478](https://github.com/lobehub/lobe-chat/issues/8478) ([768ee2b](https://github.com/lobehub/lobe-chat/commit/768ee2b))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 1.100.0](https://github.com/lobehub/lobe-chat/compare/v1.99.6...v1.100.0)
|
||||
|
||||
<sup>Released on **2025-07-17**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **misc**: Refactor desktop oauth and use JWTs token to support remote chat.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **misc**: Refactor desktop oauth and use JWTs token to support remote chat, closes [#8446](https://github.com/lobehub/lobe-chat/issues/8446) ([054ca5f](https://github.com/lobehub/lobe-chat/commit/054ca5f))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.99.6](https://github.com/lobehub/lobe-chat/compare/v1.99.5...v1.99.6)
|
||||
|
||||
<sup>Released on **2025-07-16**</sup>
|
||||
|
||||
@@ -53,6 +53,8 @@ ENV NEXT_PUBLIC_SENTRY_DSN="${NEXT_PUBLIC_SENTRY_DSN}" \
|
||||
SENTRY_ORG="" \
|
||||
SENTRY_PROJECT=""
|
||||
|
||||
ENV APP_URL="http://app.com"
|
||||
|
||||
# Posthog
|
||||
ENV NEXT_PUBLIC_ANALYTICS_POSTHOG="${NEXT_PUBLIC_ANALYTICS_POSTHOG}" \
|
||||
NEXT_PUBLIC_POSTHOG_HOST="${NEXT_PUBLIC_POSTHOG_HOST}" \
|
||||
|
||||
@@ -55,6 +55,8 @@ ENV NEXT_PUBLIC_SENTRY_DSN="${NEXT_PUBLIC_SENTRY_DSN}" \
|
||||
SENTRY_ORG="" \
|
||||
SENTRY_PROJECT=""
|
||||
|
||||
ENV APP_URL="http://app.com"
|
||||
|
||||
# Posthog
|
||||
ENV NEXT_PUBLIC_ANALYTICS_POSTHOG="${NEXT_PUBLIC_ANALYTICS_POSTHOG}" \
|
||||
NEXT_PUBLIC_POSTHOG_HOST="${NEXT_PUBLIC_POSTHOG_HOST}" \
|
||||
|
||||
@@ -383,12 +383,12 @@ In addition, these plugins are not limited to news aggregation, but can also ext
|
||||
|
||||
| Recent Submits | Description |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-05-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-07-21**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
|
||||
| [Speak](https://lobechat.com/discover/plugin/speak)<br/><sup>By **speak** on **2025-07-18**</sup> | Learn how to say anything in another language with Speak, your AI-powered language tutor.<br/>`education` `language` |
|
||||
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
|
||||
| [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
|
||||
| [Google CSE](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | Searches Google through their official CSE API.<br/>`web` `search` |
|
||||
|
||||
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
|
||||
> 📊 Total plugins: [<kbd>**43**</kbd>](https://lobechat.com/discover/plugins)
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
|
||||
@@ -376,12 +376,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
||||
|
||||
| 最近新增 | 描述 |
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-05-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-07-21**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
|
||||
| [Speak](https://lobechat.com/discover/plugin/speak)<br/><sup>By **speak** on **2025-07-18**</sup> | 使用 Speak,您的 AI 语言导师,学习如何用另一种语言说任何事情。<br/>`教育` `语言` |
|
||||
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
|
||||
| [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
|
||||
| [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网络` `搜索` |
|
||||
|
||||
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
|
||||
> 📊 Total plugins: [<kbd>**43**</kbd>](https://lobechat.com/discover/plugins)
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
|
||||
@@ -1,67 +1,353 @@
|
||||
# LobeHub Desktop Application
|
||||
# 🤯 LobeHub Desktop Application
|
||||
|
||||
LobeHub Desktop 是 [LobeChat](https://github.com/lobehub/lobe-chat) 的跨平台桌面应用程序,使用 Electron 构建,提供了更加原生的桌面体验和功能。
|
||||
LobeHub Desktop is a cross-platform desktop application for [LobeChat](https://github.com/lobehub/lobe-chat), built with Electron, providing a more native desktop experience and functionality.
|
||||
|
||||
## 功能特点
|
||||
## ✨ Features
|
||||
|
||||
- **跨平台支持**:支持 macOS (Intel/Apple Silicon)、Windows 和 Linux 系统
|
||||
- **自动更新**:内置更新机制,确保您始终使用最新版本
|
||||
- **多语言支持**:完整的国际化支持,包括中文、英文等多种语言
|
||||
- **原生集成**:与操作系统深度集成,提供原生菜单、快捷键和通知
|
||||
- **安全可靠**:macOS 版本经过公证,确保安全性
|
||||
- **多渠道发布**:提供稳定版、测试版和每日构建版本
|
||||
- **🌍 Cross-platform Support**: Supports macOS (Intel/Apple Silicon), Windows, and Linux systems
|
||||
- **🔄 Auto Updates**: Built-in update mechanism ensures you always have the latest version
|
||||
- **🌐 Multi-language Support**: Complete i18n support for 18+ languages with lazy loading
|
||||
- **🎨 Native Integration**: Deep OS integration with native menus, shortcuts, and notifications
|
||||
- **🔒 Secure & Reliable**: macOS notarized, encrypted token storage, secure OAuth flow
|
||||
- **📦 Multiple Release Channels**: Stable, beta, and nightly build versions
|
||||
- **⚡ Advanced Window Management**: Multi-window architecture with theme synchronization
|
||||
- **🔗 Remote Server Sync**: Secure data synchronization with remote LobeChat instances
|
||||
- **🎯 Developer Tools**: Built-in development panel and comprehensive debugging tools
|
||||
|
||||
## 开发环境设置
|
||||
## 🚀 Development Setup
|
||||
|
||||
### 前提条件
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 22+
|
||||
- pnpm 10+
|
||||
- **Node.js** 22+
|
||||
- **pnpm** 10+
|
||||
- **Electron** compatible development environment
|
||||
|
||||
### 安装依赖
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install-isolated
|
||||
```
|
||||
|
||||
### 配置环境变量
|
||||
|
||||
复制 `.env.desktop` 到 `.env`。
|
||||
|
||||
> [!WARNING]
|
||||
> 注意提前备份好 `.env` 文件,避免丢失配置。
|
||||
|
||||
### 开发模式运行
|
||||
|
||||
```bash
|
||||
# Start development server
|
||||
pnpm electron:dev
|
||||
|
||||
# Type checking
|
||||
pnpm typecheck
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### 构建应用
|
||||
### Environment Configuration
|
||||
|
||||
构建所有平台:
|
||||
Copy `.env.desktop` to `.env` and configure as needed:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
cp .env.desktop .env
|
||||
```
|
||||
|
||||
构建特定平台:
|
||||
> \[!WARNING]
|
||||
> Backup your `.env` file before making changes to avoid losing configurations.
|
||||
|
||||
### Build Commands
|
||||
|
||||
| Command | Description |
|
||||
| ------------------ | --------------------------------------- |
|
||||
| `pnpm build` | Build for all platforms |
|
||||
| `pnpm build:mac` | Build for macOS (Intel + Apple Silicon) |
|
||||
| `pnpm build:win` | Build for Windows |
|
||||
| `pnpm build:linux` | Build for Linux |
|
||||
| `pnpm build-local` | Local development build |
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
pnpm build:mac
|
||||
# 1. Development
|
||||
pnpm electron:dev # Start with hot reload
|
||||
|
||||
# Windows
|
||||
pnpm build:win
|
||||
# 2. Code Quality
|
||||
pnpm lint # ESLint checking
|
||||
pnpm format # Prettier formatting
|
||||
pnpm typecheck # TypeScript validation
|
||||
|
||||
# Linux
|
||||
pnpm build:linux
|
||||
# 3. Testing
|
||||
pnpm test # Run Vitest tests
|
||||
|
||||
# 4. Build & Package
|
||||
pnpm build # Production build
|
||||
pnpm build-local # Local testing build
|
||||
```
|
||||
|
||||
## 发布渠道
|
||||
## 🎯 Release Channels
|
||||
|
||||
应用提供三个发布渠道:
|
||||
| Channel | Description | Stability | Auto-Updates |
|
||||
| ----------- | -------------------------------- | --------- | ------------ |
|
||||
| **Stable** | Thoroughly tested releases | 🟢 High | ✅ Yes |
|
||||
| **Beta** | Pre-release with new features | 🟡 Medium | ✅ Yes |
|
||||
| **Nightly** | Daily builds with latest changes | 🟠 Low | ✅ Yes |
|
||||
|
||||
- **稳定版**:经过充分测试的正式版本
|
||||
- **测试版 (Beta)**:预发布版本,包含即将发布的新功能
|
||||
- **每日构建版 (Nightly)**:包含最新开发进展的构建版本
|
||||
## 🛠 Technology Stack
|
||||
|
||||
### Core Framework
|
||||
|
||||
- **Electron** `37.1.0` - Cross-platform desktop framework
|
||||
- **Node.js** `22+` - Backend runtime
|
||||
- **TypeScript** `5.7+` - Type-safe development
|
||||
- **Vite** `6.2+` - Build tooling
|
||||
|
||||
### Architecture & Patterns
|
||||
|
||||
- **Dependency Injection** - IoC container with decorator-based registration
|
||||
- **Event-Driven Architecture** - IPC communication between processes
|
||||
- **Module Federation** - Dynamic controller and service loading
|
||||
- **Observer Pattern** - State management and UI synchronization
|
||||
|
||||
### Development Tools
|
||||
|
||||
- **Vitest** - Unit testing framework
|
||||
- **ESLint** - Code linting
|
||||
- **Prettier** - Code formatting
|
||||
- **electron-builder** - Application packaging
|
||||
- **electron-updater** - Auto-update mechanism
|
||||
|
||||
### Security & Storage
|
||||
|
||||
- **Electron Safe Storage** - Encrypted token storage
|
||||
- **OAuth 2.0 + PKCE** - Secure authentication flow
|
||||
- **electron-store** - Persistent configuration
|
||||
- **Custom Protocol Handler** - Secure callback handling
|
||||
|
||||
## 🏗 Architecture
|
||||
|
||||
The desktop application uses a sophisticated dependency injection and event-driven architecture:
|
||||
|
||||
### 📁 Core Structure
|
||||
|
||||
```
|
||||
src/main/core/
|
||||
├── App.ts # 🎯 Main application orchestrator
|
||||
├── IoCContainer.ts # 🔌 Dependency injection container
|
||||
├── window/ # 🪟 Window management modules
|
||||
│ ├── WindowThemeManager.ts # 🎨 Theme synchronization
|
||||
│ ├── WindowPositionManager.ts # 📐 Position persistence
|
||||
│ ├── WindowErrorHandler.ts # ⚠️ Error boundaries
|
||||
│ └── WindowConfigBuilder.ts # ⚙️ Configuration builder
|
||||
├── browser/ # 🌐 Browser management modules
|
||||
│ ├── Browser.ts # 🪟 Individual window instances
|
||||
│ └── BrowserManager.ts # 👥 Multi-window coordinator
|
||||
├── ui/ # 🎨 UI system modules
|
||||
│ ├── Tray.ts # 📍 System tray integration
|
||||
│ ├── TrayManager.ts # 🔧 Tray management
|
||||
│ ├── MenuManager.ts # 📋 Native menu system
|
||||
│ └── ShortcutManager.ts # ⌨️ Global shortcuts
|
||||
└── infrastructure/ # 🔧 Infrastructure services
|
||||
├── StoreManager.ts # 💾 Configuration storage
|
||||
├── I18nManager.ts # 🌍 Internationalization
|
||||
├── UpdaterManager.ts # 📦 Auto-update system
|
||||
└── StaticFileServerManager.ts # 🗂️ Local file serving
|
||||
```
|
||||
|
||||
### 🔄 Application Lifecycle
|
||||
|
||||
The `App.ts` class orchestrates the entire application lifecycle through key phases:
|
||||
|
||||
#### 1. 🚀 Initialization Phase
|
||||
|
||||
- **System Information Logging** - Captures OS, CPU, RAM, and locale details
|
||||
- **Store Manager Setup** - Initializes persistent configuration storage
|
||||
- **Dynamic Module Loading** - Auto-discovers controllers and services via glob imports
|
||||
- **IPC Event Registration** - Sets up inter-process communication channels
|
||||
|
||||
#### 2. 🏃 Bootstrap Phase
|
||||
|
||||
- **Single Instance Check** - Ensures only one application instance runs
|
||||
- **IPC Server Launch** - Starts the communication server
|
||||
- **Core Manager Initialization** - Sequential initialization of all managers:
|
||||
- 🌍 I18n for internationalization
|
||||
- 📋 Menu system for native menus
|
||||
- 🗂️ Static file server for local assets
|
||||
- ⌨️ Global shortcuts registration
|
||||
- 🪟 Browser window management
|
||||
- 📍 System tray (Windows only)
|
||||
- 📦 Auto-updater system
|
||||
|
||||
### 🔧 Core Components Deep Dive
|
||||
|
||||
#### 🌐 Browser Management System
|
||||
|
||||
- **Multi-Window Architecture** - Supports chat, settings, and devtools windows
|
||||
- **Window State Management** - Handles positioning, theming, and lifecycle
|
||||
- **WebContents Mapping** - Bidirectional mapping between WebContents and identifiers
|
||||
- **Event Broadcasting** - Centralized event distribution to all or specific windows
|
||||
|
||||
#### 🔌 Dependency Injection & Event System
|
||||
|
||||
- **IoC Container** - WeakMap-based container for decorated controller methods
|
||||
- **Decorator Registration** - `@ipcClientEvent` and `@ipcServerEvent` decorators
|
||||
- **Automatic Event Mapping** - Events registered during controller loading
|
||||
- **Service Locator** - Type-safe service and controller retrieval
|
||||
|
||||
#### 🪟 Window Management
|
||||
|
||||
- **Theme-Aware Windows** - Automatic adaptation to system dark/light mode
|
||||
- **Platform-Specific Styling** - Windows title bar and overlay customization
|
||||
- **Position Persistence** - Save and restore window positions across sessions
|
||||
- **Error Boundaries** - Centralized error handling for window operations
|
||||
|
||||
#### 🔧 Infrastructure Services
|
||||
|
||||
##### 🌍 I18n Manager
|
||||
|
||||
- **18+ Language Support** with lazy loading and namespace organization
|
||||
- **System Integration** with Electron's locale detection
|
||||
- **Dynamic UI Refresh** on language changes
|
||||
- **Resource Management** with efficient loading strategies
|
||||
|
||||
##### 📦 Update Manager
|
||||
|
||||
- **Multi-Channel Support** (stable, beta, nightly) with configurable intervals
|
||||
- **Background Downloads** with progress tracking and user notifications
|
||||
- **Rollback Protection** with error handling and recovery mechanisms
|
||||
- **Channel Management** with automatic channel switching
|
||||
|
||||
##### 💾 Store Manager
|
||||
|
||||
- **Type-Safe Storage** using electron-store with TypeScript interfaces
|
||||
- **Encrypted Secrets** via Electron's Safe Storage API
|
||||
- **Configuration Validation** with default value management
|
||||
- **File System Integration** with automatic directory creation
|
||||
|
||||
##### 🗂️ Static File Server
|
||||
|
||||
- **Local HTTP Server** for serving application assets and user files
|
||||
- **Security Controls** with request filtering and access validation
|
||||
- **File Management** with upload, download, and deletion capabilities
|
||||
- **Path Resolution** with intelligent routing between storage locations
|
||||
|
||||
#### 🎨 UI System Integration
|
||||
|
||||
- **Global Shortcuts** - Platform-aware keyboard shortcut registration with conflict detection
|
||||
- **System Tray** - Native integration with context menus and notifications
|
||||
- **Native Menus** - Platform-specific application and context menus with i18n
|
||||
- **Theme Synchronization** - Automatic theme updates across all UI components
|
||||
|
||||
### 🏛 Controller & Service Architecture
|
||||
|
||||
#### 🎮 Controller Pattern
|
||||
|
||||
- **IPC Event Handling** - Processes events from renderer with decorator-based registration
|
||||
- **Lifecycle Hooks** - `beforeAppReady` and `afterAppReady` for initialization phases
|
||||
- **Type-Safe Communication** - Strong typing for all IPC events and responses
|
||||
- **Error Boundaries** - Comprehensive error handling with proper propagation
|
||||
|
||||
#### 🔧 Service Pattern
|
||||
|
||||
- **Business Logic Encapsulation** - Clean separation of concerns
|
||||
- **Dependency Management** - Managed through IoC container
|
||||
- **Cross-Controller Sharing** - Services accessible via service locator pattern
|
||||
- **Resource Management** - Proper initialization and cleanup
|
||||
|
||||
### 🔗 Inter-Process Communication
|
||||
|
||||
#### 📡 IPC System Features
|
||||
|
||||
- **Bidirectional Communication** - Main↔Renderer and Main↔Next.js server
|
||||
- **Type-Safe Events** - TypeScript interfaces for all event parameters
|
||||
- **Context Awareness** - Events include sender context for window-specific operations
|
||||
- **Error Propagation** - Centralized error handling with proper status codes
|
||||
|
||||
#### 🛡️ Security Features
|
||||
|
||||
- **OAuth 2.0 + PKCE** - Secure authentication with state parameter validation
|
||||
- **Encrypted Token Storage** - Using Electron's Safe Storage API when available
|
||||
- **Custom Protocol Handler** - Secure callback handling for OAuth flows
|
||||
- **Request Filtering** - Security controls for web requests and external links
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Test Structure
|
||||
|
||||
```bash
|
||||
apps/desktop/src/main/controllers/__tests__/ # Controller unit tests
|
||||
tests/ # Integration tests
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
pnpm test # Run all tests
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm typecheck # Type validation
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- **Controller Tests** - IPC event handling validation
|
||||
- **Service Tests** - Business logic verification
|
||||
- **Integration Tests** - End-to-end workflow testing
|
||||
- **Type Tests** - TypeScript interface validation
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- **OAuth 2.0 Flow** with PKCE for secure token exchange
|
||||
- **State Parameter Validation** to prevent CSRF attacks
|
||||
- **Encrypted Token Storage** using platform-native secure storage
|
||||
- **Automatic Token Refresh** with fallback to re-authentication
|
||||
|
||||
### Application Security
|
||||
|
||||
- **Code Signing** - macOS notarization for enhanced security
|
||||
- **Sandboxing** - Controlled access to system resources
|
||||
- **CSP Controls** - Content Security Policy management
|
||||
- **Request Filtering** - Security controls for external requests
|
||||
|
||||
### Data Protection
|
||||
|
||||
- **Encrypted Configuration** - Sensitive data encrypted at rest
|
||||
- **Secure IPC** - Type-safe communication channels
|
||||
- **Path Validation** - Secure file system access controls
|
||||
- **Network Security** - HTTPS enforcement and proxy support
|
||||
|
||||
## 🤝 Contribution
|
||||
|
||||
Desktop application development involves complex cross-platform considerations and native integrations. We welcome community contributions to improve functionality, performance, and user experience. You can participate in improvements through:
|
||||
|
||||
### How to Contribute
|
||||
|
||||
1. **Platform Support**: Enhance cross-platform compatibility and native integrations
|
||||
2. **Performance Optimization**: Improve application startup time, memory usage, and responsiveness
|
||||
3. **Feature Development**: Add new desktop-specific features and capabilities
|
||||
4. **Bug Fixes**: Fix platform-specific issues and edge cases
|
||||
5. **Security Improvements**: Enhance security measures and authentication flows
|
||||
6. **UI/UX Enhancements**: Improve desktop user interface and experience
|
||||
|
||||
### Contribution Process
|
||||
|
||||
1. Fork the [LobeChat repository](https://github.com/lobehub/lobe-chat)
|
||||
2. Set up the desktop development environment following our setup guide
|
||||
3. Make your changes to the desktop application
|
||||
4. Submit a Pull Request describing:
|
||||
|
||||
- Platform compatibility testing results
|
||||
- Performance impact analysis
|
||||
- Security considerations
|
||||
- User experience improvements
|
||||
- Breaking changes (if any)
|
||||
|
||||
### Development Areas
|
||||
|
||||
- **Core Architecture**: Dependency injection, event system, and lifecycle management
|
||||
- **Window Management**: Multi-window support, theme synchronization, and state persistence
|
||||
- **IPC Communication**: Type-safe inter-process communication between main and renderer
|
||||
- **Platform Integration**: Native menus, shortcuts, notifications, and system tray
|
||||
- **Security Features**: OAuth flows, token encryption, and secure storage
|
||||
- **Auto-Update System**: Multi-channel updates and rollback mechanisms
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- **Development Guide**: [`Development.md`](./Development.md) - Comprehensive development documentation
|
||||
- **Architecture Docs**: [`/docs`](../../docs/) - Detailed technical specifications
|
||||
- **Contributing**: [`CONTRIBUTING.md`](../../CONTRIBUTING.md) - Contribution guidelines
|
||||
- **Issues & Support**: [GitHub Issues](https://github.com/lobehub/lobe-chat/issues)
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
# 🤯 LobeHub 桌面应用程序
|
||||
|
||||
LobeHub Desktop 是 [LobeChat](https://github.com/lobehub/lobe-chat) 的跨平台桌面应用程序,使用 Electron 构建,提供了更加原生的桌面体验和功能。
|
||||
|
||||
## ✨ 功能特点
|
||||
|
||||
- **🌍 跨平台支持**:支持 macOS (Intel/Apple Silicon)、Windows 和 Linux 系统
|
||||
- **🔄 自动更新**:内置更新机制,确保您始终使用最新版本
|
||||
- **🌐 多语言支持**:完整的 i18n 支持,包含 18+ 种语言的懒加载
|
||||
- **🎨 原生集成**:与操作系统深度集成,提供原生菜单、快捷键和通知
|
||||
- **🔒 安全可靠**:macOS 公证认证,加密令牌存储,安全的 OAuth 流程
|
||||
- **📦 多渠道发布**:提供稳定版、测试版和每日构建版本
|
||||
- **⚡ 高级窗口管理**:多窗口架构,支持主题同步
|
||||
- **🔗 远程服务器同步**:与远程 LobeChat 实例的安全数据同步
|
||||
- **🎯 开发者工具**:内置开发面板和全面的调试工具
|
||||
|
||||
## 🚀 开发环境设置
|
||||
|
||||
### 前提条件
|
||||
|
||||
- **Node.js** 22+
|
||||
- **pnpm** 10+
|
||||
- **Electron** 兼容的开发环境
|
||||
|
||||
### 快速开始
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install-isolated
|
||||
|
||||
# 启动开发服务器
|
||||
pnpm electron:dev
|
||||
|
||||
# 类型检查
|
||||
pnpm typecheck
|
||||
|
||||
# 运行测试
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
复制 `.env.desktop` 到 `.env` 并根据需要配置:
|
||||
|
||||
```bash
|
||||
cp .env.desktop .env
|
||||
```
|
||||
|
||||
> \[!WARNING]
|
||||
> 在进行更改之前请备份您的 `.env` 文件,避免丢失配置。
|
||||
|
||||
### 构建命令
|
||||
|
||||
| 命令 | 描述 |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| `pnpm build` | 构建所有平台 |
|
||||
| `pnpm build:mac` | 构建 macOS (Intel + Apple Silicon) |
|
||||
| `pnpm build:win` | 构建 Windows |
|
||||
| `pnpm build:linux` | 构建 Linux |
|
||||
| `pnpm build-local` | 本地开发构建 |
|
||||
|
||||
### 开发工作流
|
||||
|
||||
```bash
|
||||
# 1. 开发
|
||||
pnpm electron:dev # 启动热重载开发服务器
|
||||
|
||||
# 2. 代码质量
|
||||
pnpm lint # ESLint 检查
|
||||
pnpm format # Prettier 格式化
|
||||
pnpm typecheck # TypeScript 验证
|
||||
|
||||
# 3. 测试
|
||||
pnpm test # 运行 Vitest 测试
|
||||
|
||||
# 4. 构建和打包
|
||||
pnpm build # 生产构建
|
||||
pnpm build-local # 本地测试构建
|
||||
```
|
||||
|
||||
## 🎯 发布渠道
|
||||
|
||||
| 渠道 | 描述 | 稳定性 | 自动更新 |
|
||||
| ------------------------ | ---------------------- | ------ | -------- |
|
||||
| **稳定版** | 经过充分测试的正式版本 | 🟢 高 | ✅ 是 |
|
||||
| **测试版 (Beta)** | 包含新功能的预发布版本 | 🟡 中 | ✅ 是 |
|
||||
| **每日构建版 (Nightly)** | 包含最新更改的每日构建 | 🟠 低 | ✅ 是 |
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
### 核心框架
|
||||
|
||||
- **Electron** `37.1.0` - 跨平台桌面框架
|
||||
- **Node.js** `22+` - 后端运行时
|
||||
- **TypeScript** `5.7+` - 类型安全开发
|
||||
- **Vite** `6.2+` - 构建工具
|
||||
|
||||
### 架构和模式
|
||||
|
||||
- **依赖注入** - 基于装饰器注册的 IoC 容器
|
||||
- **事件驱动架构** - 进程间 IPC 通信
|
||||
- **模块联邦** - 动态控制器和服务加载
|
||||
- **观察者模式** - 状态管理和 UI 同步
|
||||
|
||||
### 开发工具
|
||||
|
||||
- **Vitest** - 单元测试框架
|
||||
- **ESLint** - 代码检查
|
||||
- **Prettier** - 代码格式化
|
||||
- **electron-builder** - 应用程序打包
|
||||
- **electron-updater** - 自动更新机制
|
||||
|
||||
### 安全和存储
|
||||
|
||||
- **Electron Safe Storage** - 加密令牌存储
|
||||
- **OAuth 2.0 + PKCE** - 安全认证流程
|
||||
- **electron-store** - 持久化配置
|
||||
- **自定义协议处理器** - 安全回调处理
|
||||
|
||||
## 🏗 架构设计
|
||||
|
||||
桌面应用程序采用了复杂的依赖注入和事件驱动架构:
|
||||
|
||||
### 📁 核心结构
|
||||
|
||||
```
|
||||
src/main/core/
|
||||
├── App.ts # 🎯 主应用程序协调器
|
||||
├── IoCContainer.ts # 🔌 依赖注入容器
|
||||
├── window/ # 🪟 窗口管理模块
|
||||
│ ├── WindowThemeManager.ts # 🎨 主题同步
|
||||
│ ├── WindowPositionManager.ts # 📐 位置持久化
|
||||
│ ├── WindowErrorHandler.ts # ⚠️ 错误边界
|
||||
│ └── WindowConfigBuilder.ts # ⚙️ 配置构建器
|
||||
├── browser/ # 🌐 浏览器管理模块
|
||||
│ ├── Browser.ts # 🪟 单个窗口实例
|
||||
│ └── BrowserManager.ts # 👥 多窗口协调器
|
||||
├── ui/ # 🎨 UI 系统模块
|
||||
│ ├── Tray.ts # 📍 系统托盘集成
|
||||
│ ├── TrayManager.ts # 🔧 托盘管理
|
||||
│ ├── MenuManager.ts # 📋 原生菜单系统
|
||||
│ └── ShortcutManager.ts # ⌨️ 全局快捷键
|
||||
└── infrastructure/ # 🔧 基础设施服务
|
||||
├── StoreManager.ts # 💾 配置存储
|
||||
├── I18nManager.ts # 🌍 国际化
|
||||
├── UpdaterManager.ts # 📦 自动更新系统
|
||||
└── StaticFileServerManager.ts # 🗂️ 本地文件服务
|
||||
```
|
||||
|
||||
### 🔄 应用程序生命周期
|
||||
|
||||
`App.ts` 类通过几个关键阶段协调整个应用程序的生命周期:
|
||||
|
||||
#### 1. 🚀 初始化阶段
|
||||
|
||||
- **系统信息记录** - 捕获操作系统、CPU、内存和区域设置详细信息
|
||||
- **存储管理器设置** - 初始化持久配置存储
|
||||
- **动态模块加载** - 通过 glob 导入自动发现控制器和服务
|
||||
- **IPC 事件注册** - 设置进程间通信通道
|
||||
|
||||
#### 2. 🏃 引导阶段
|
||||
|
||||
- **单实例检查** - 确保只运行一个应用程序实例
|
||||
- **IPC 服务器启动** - 启动通信服务器
|
||||
- **核心管理器初始化** - 按顺序初始化所有管理器:
|
||||
- 🌍 国际化管理器
|
||||
- 📋 原生菜单系统
|
||||
- 🗂️ 本地资源服务器
|
||||
- ⌨️ 全局快捷键注册
|
||||
- 🪟 浏览器窗口管理
|
||||
- 📍 系统托盘(仅 Windows)
|
||||
- 📦 自动更新系统
|
||||
|
||||
### 🔧 核心组件深度解析
|
||||
|
||||
#### 🌐 浏览器管理系统
|
||||
|
||||
- **多窗口架构** - 支持聊天、设置和开发工具窗口
|
||||
- **窗口状态管理** - 处理定位、主题和生命周期
|
||||
- **WebContents 映射** - WebContents 和标识符之间的双向映射
|
||||
- **事件广播** - 向所有或特定窗口的集中事件分发
|
||||
|
||||
#### 🔌 依赖注入和事件系统
|
||||
|
||||
- **IoC 容器** - 基于 WeakMap 的装饰控制器方法容器
|
||||
- **装饰器注册** - `@ipcClientEvent` 和 `@ipcServerEvent` 装饰器
|
||||
- **自动事件映射** - 控制器加载期间注册的事件
|
||||
- **服务定位器** - 类型安全的服务和控制器检索
|
||||
|
||||
#### 🪟 窗口管理
|
||||
|
||||
- **主题感知窗口** - 自动适应系统深色 / 浅色模式
|
||||
- **平台特定样式** - Windows 标题栏和覆盖自定义
|
||||
- **位置持久化** - 跨会话保存和恢复窗口位置
|
||||
- **错误边界** - 窗口操作的集中错误处理
|
||||
|
||||
#### 🔧 基础设施服务
|
||||
|
||||
##### 🌍 国际化管理器
|
||||
|
||||
- **18+ 语言支持** 懒加载和命名空间组织
|
||||
- **系统集成** 与 Electron 的区域检测集成
|
||||
- **动态 UI 刷新** 语言更改时的 UI 更新
|
||||
- **资源管理** 高效的加载策略
|
||||
|
||||
##### 📦 更新管理器
|
||||
|
||||
- **多渠道支持** (稳定版、测试版、每日构建)可配置间隔
|
||||
- **后台下载** 进度跟踪和用户通知
|
||||
- **回滚保护** 错误处理和恢复机制
|
||||
- **渠道管理** 自动渠道切换
|
||||
|
||||
##### 💾 存储管理器
|
||||
|
||||
- **类型安全存储** 使用带有 TypeScript 接口的 electron-store
|
||||
- **加密机密** 通过 Electron 的安全存储 API
|
||||
- **配置验证** 默认值管理
|
||||
- **文件系统集成** 自动目录创建
|
||||
|
||||
##### 🗂️ 静态文件服务器
|
||||
|
||||
- **本地 HTTP 服务器** 用于提供应用程序资源和用户文件
|
||||
- **安全控制** 请求过滤和访问验证
|
||||
- **文件管理** 上传、下载和删除功能
|
||||
- **路径解析** 存储位置之间的智能路由
|
||||
|
||||
#### 🎨 UI 系统集成
|
||||
|
||||
- **全局快捷键** - 平台感知的键盘快捷键注册与冲突检测
|
||||
- **系统托盘** - 带有上下文菜单和通知的原生集成
|
||||
- **原生菜单** - 带有 i18n 的平台特定应用程序和上下文菜单
|
||||
- **主题同步** - 所有 UI 组件的自动主题更新
|
||||
|
||||
### 🏛 控制器和服务架构
|
||||
|
||||
#### 🎮 控制器模式
|
||||
|
||||
- **IPC 事件处理** - 通过基于装饰器的注册处理来自渲染器的事件
|
||||
- **生命周期钩子** - 初始化阶段的 `beforeAppReady` 和 `afterAppReady`
|
||||
- **类型安全通信** - 所有 IPC 事件和响应的强类型
|
||||
- **错误边界** - 具有适当传播的全面错误处理
|
||||
|
||||
#### 🔧 服务模式
|
||||
|
||||
- **业务逻辑封装** - 关注点的清晰分离
|
||||
- **依赖管理** - 通过 IoC 容器管理
|
||||
- **跨控制器共享** - 通过服务定位器模式访问的服务
|
||||
- **资源管理** - 适当的初始化和清理
|
||||
|
||||
### 🔗 进程间通信
|
||||
|
||||
#### 📡 IPC 系统功能
|
||||
|
||||
- **双向通信** - Main↔Renderer 和 Main↔Next.js 服务器
|
||||
- **类型安全事件** - 所有事件参数的 TypeScript 接口
|
||||
- **上下文感知** - 事件包含用于窗口特定操作的发送者上下文
|
||||
- **错误传播** - 具有适当状态码的集中错误处理
|
||||
|
||||
#### 🛡️ 安全功能
|
||||
|
||||
- **OAuth 2.0 + PKCE** - 具有状态参数验证的安全认证
|
||||
- **加密令牌存储** - 在可用时使用 Electron 的安全存储 API
|
||||
- **自定义协议处理器** - OAuth 流程的安全回调处理
|
||||
- **请求过滤** - 网络请求和外部链接的安全控制
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 测试结构
|
||||
|
||||
```bash
|
||||
apps/desktop/src/main/controllers/__tests__/ # 控制器单元测试
|
||||
tests/ # 集成测试
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
pnpm test # 运行所有测试
|
||||
pnpm test:watch # 监视模式
|
||||
pnpm typecheck # 类型验证
|
||||
```
|
||||
|
||||
### 测试覆盖
|
||||
|
||||
- **控制器测试** - IPC 事件处理验证
|
||||
- **服务测试** - 业务逻辑验证
|
||||
- **集成测试** - 端到端工作流测试
|
||||
- **类型测试** - TypeScript 接口验证
|
||||
|
||||
## 🔒 安全功能
|
||||
|
||||
### 认证和授权
|
||||
|
||||
- **OAuth 2.0 流程** 使用 PKCE 进行安全令牌交换
|
||||
- **状态参数验证** 防止 CSRF 攻击
|
||||
- **加密令牌存储** 使用平台原生安全存储
|
||||
- **自动令牌刷新** 在失败时回退到重新认证
|
||||
|
||||
### 应用程序安全
|
||||
|
||||
- **代码签名** - macOS 公证认证以增强安全性
|
||||
- **沙盒** - 对系统资源的受控访问
|
||||
- **CSP 控制** - 内容安全策略管理
|
||||
- **请求过滤** - 外部请求的安全控制
|
||||
|
||||
### 数据保护
|
||||
|
||||
- **加密配置** - 敏感数据静态加密
|
||||
- **安全 IPC** - 类型安全的通信通道
|
||||
- **路径验证** - 安全的文件系统访问控制
|
||||
- **网络安全** - HTTPS 强制和代理支持
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
桌面应用程序开发涉及复杂的跨平台考虑和原生集成。我们欢迎社区贡献来改进功能、性能和用户体验。您可以通过以下方式参与改进:
|
||||
|
||||
### 如何贡献
|
||||
|
||||
1. **平台支持**:增强跨平台兼容性和原生集成
|
||||
2. **性能优化**:改进应用程序启动时间、内存使用和响应性
|
||||
3. **功能开发**:添加新的桌面特定功能和能力
|
||||
4. **错误修复**:修复平台特定问题和边缘情况
|
||||
5. **安全改进**:增强安全措施和认证流程
|
||||
6. **UI/UX 增强**:改进桌面用户界面和体验
|
||||
|
||||
### 贡献流程
|
||||
|
||||
1. Fork [LobeChat 仓库](https://github.com/lobehub/lobe-chat)
|
||||
2. 按照我们的设置指南建立桌面开发环境
|
||||
3. 对桌面应用程序进行修改
|
||||
4. 提交 Pull Request 并描述:
|
||||
|
||||
- 平台兼容性测试结果
|
||||
- 性能影响分析
|
||||
- 安全考虑
|
||||
- 用户体验改进
|
||||
- 破坏性更改(如有)
|
||||
|
||||
### 开发领域
|
||||
|
||||
- **核心架构**:依赖注入、事件系统和生命周期管理
|
||||
- **窗口管理**:多窗口支持、主题同步和状态持久化
|
||||
- **IPC 通信**:主进程和渲染进程之间的类型安全进程间通信
|
||||
- **平台集成**:原生菜单、快捷键、通知和系统托盘
|
||||
- **安全功能**:OAuth 流程、令牌加密和安全存储
|
||||
- **自动更新系统**:多渠道更新和回滚机制
|
||||
|
||||
## 📚 其他资源
|
||||
|
||||
- **开发指南**:[`Development.md`](./Development.md) - 全面的开发文档
|
||||
- **架构文档**:[`/docs`](../../docs/) - 详细的技术规范
|
||||
- **贡献指南**:[`CONTRIBUTING.md`](../../CONTRIBUTING.md) - 贡献指导
|
||||
- **问题和支持**:[GitHub Issues](https://github.com/lobehub/lobe-chat/issues)
|
||||
|
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: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "lobehub-desktop-dev",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "LobeHub Desktop Application",
|
||||
"homepage": "https://lobehub.com",
|
||||
"repository": {
|
||||
@@ -30,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"
|
||||
},
|
||||
|
||||
|
After Width: | Height: | Size: 807 B |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 738 B |
|
After Width: | Height: | Size: 2.5 KiB |
@@ -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,3 +1,29 @@
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
import { dev, linux, macOS, windows } from 'electron-is';
|
||||
import os from 'node:os';
|
||||
|
||||
export const isDev = dev();
|
||||
|
||||
export const OFFICIAL_CLOUD_SERVER = process.env.OFFICIAL_CLOUD_SERVER || 'https://lobechat.com';
|
||||
|
||||
export const isMac = macOS();
|
||||
export const isWindows = windows();
|
||||
export const isLinux = linux();
|
||||
|
||||
function getIsWindows11() {
|
||||
if (!isWindows) return false;
|
||||
// 获取操作系统版本(如 "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 开始
|
||||
return majorVersion === 10 && minorVersion === 0 && buildNumber >= 22_000;
|
||||
}
|
||||
|
||||
export const isWindows11 = getIsWindows11();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
DesktopNotificationResult,
|
||||
ShowDesktopNotificationParams,
|
||||
} from '@lobechat/electron-client-ipc';
|
||||
import { Notification, app } from 'electron';
|
||||
import { macOS, windows } from 'electron-is';
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
|
||||
const logger = createLogger('controllers:NotificationCtr');
|
||||
|
||||
export default class NotificationCtr extends ControllerModule {
|
||||
/**
|
||||
* 在应用准备就绪后设置桌面通知
|
||||
*/
|
||||
afterAppReady() {
|
||||
this.setupNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置桌面通知权限和配置
|
||||
*/
|
||||
private setupNotifications() {
|
||||
logger.debug('Setting up desktop notifications');
|
||||
|
||||
try {
|
||||
// 检查通知支持
|
||||
if (!Notification.isSupported()) {
|
||||
logger.warn('Desktop notifications are not supported on this platform');
|
||||
return;
|
||||
}
|
||||
|
||||
// 在 macOS 上,我们可能需要显式请求通知权限
|
||||
if (macOS()) {
|
||||
logger.debug('macOS detected, notification permissions should be handled by system');
|
||||
}
|
||||
|
||||
// 在 Windows 上设置应用用户模型 ID
|
||||
if (windows()) {
|
||||
app.setAppUserModelId('com.lobehub.chat');
|
||||
logger.debug('Set Windows App User Model ID for notifications');
|
||||
}
|
||||
|
||||
logger.info('Desktop notifications setup completed');
|
||||
} catch (error) {
|
||||
logger.error('Failed to setup desktop notifications:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 显示系统桌面通知(仅当窗口隐藏时)
|
||||
*/
|
||||
@ipcClientEvent('showDesktopNotification')
|
||||
async showDesktopNotification(
|
||||
params: ShowDesktopNotificationParams,
|
||||
): Promise<DesktopNotificationResult> {
|
||||
logger.debug('收到桌面通知请求:', params);
|
||||
|
||||
try {
|
||||
// 检查通知支持
|
||||
if (!Notification.isSupported()) {
|
||||
logger.warn('系统不支持桌面通知');
|
||||
return { error: 'Desktop notifications not supported', success: false };
|
||||
}
|
||||
|
||||
// 检查窗口是否隐藏
|
||||
const isWindowHidden = this.isMainWindowHidden();
|
||||
|
||||
if (!isWindowHidden) {
|
||||
logger.debug('主窗口可见,跳过桌面通知');
|
||||
return { reason: 'Window is visible', skipped: true, success: true };
|
||||
}
|
||||
|
||||
logger.info('窗口已隐藏,显示桌面通知:', params.title);
|
||||
|
||||
const notification = new Notification({
|
||||
body: params.body,
|
||||
// 添加更多配置以确保通知能正常显示
|
||||
hasReply: false,
|
||||
silent: params.silent || false,
|
||||
timeoutType: 'default',
|
||||
title: params.title,
|
||||
urgency: 'normal',
|
||||
});
|
||||
|
||||
// 添加更多事件监听来调试
|
||||
notification.on('show', () => {
|
||||
logger.info('通知已显示');
|
||||
});
|
||||
|
||||
notification.on('click', () => {
|
||||
logger.debug('用户点击通知,显示主窗口');
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.show();
|
||||
mainWindow.browserWindow.focus();
|
||||
});
|
||||
|
||||
notification.on('close', () => {
|
||||
logger.debug('通知已关闭');
|
||||
});
|
||||
|
||||
notification.on('failed', (error) => {
|
||||
logger.error('通知显示失败:', error);
|
||||
});
|
||||
|
||||
// 使用 Promise 来确保通知显示
|
||||
return new Promise((resolve) => {
|
||||
notification.show();
|
||||
|
||||
// 给通知一些时间来显示,然后检查结果
|
||||
setTimeout(() => {
|
||||
logger.info('通知显示调用完成');
|
||||
resolve({ success: true });
|
||||
}, 100);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('显示桌面通知失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查主窗口是否隐藏
|
||||
*/
|
||||
@ipcClientEvent('isMainWindowHidden')
|
||||
isMainWindowHidden(): boolean {
|
||||
try {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
const browserWindow = mainWindow.browserWindow;
|
||||
|
||||
// 如果窗口被销毁,认为是隐藏的
|
||||
if (browserWindow.isDestroyed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查窗口是否可见和聚焦
|
||||
const isVisible = browserWindow.isVisible();
|
||||
const isFocused = browserWindow.isFocused();
|
||||
const isMinimized = browserWindow.isMinimized();
|
||||
|
||||
logger.debug('窗口状态检查:', { isFocused, isMinimized, isVisible });
|
||||
|
||||
// 窗口隐藏的条件:不可见或最小化或失去焦点
|
||||
return !isVisible || isMinimized || !isFocused;
|
||||
} catch (error) {
|
||||
logger.error('检查窗口状态失败:', error);
|
||||
return true; // 发生错误时认为窗口隐藏,确保通知能显示
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,8 +334,7 @@ export default class RemoteServerSyncCtr extends ControllerModule {
|
||||
method: method,
|
||||
path: url.pathname + url.search,
|
||||
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||
protocol: url.protocol, // 注入代理
|
||||
// agent: false, // Consider for keep-alive issues if they arise
|
||||
protocol: url.protocol,
|
||||
};
|
||||
|
||||
const requester = url.protocol === 'https:' ? https : http;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
|
||||
import { nativeTheme } from 'electron';
|
||||
|
||||
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');
|
||||
@@ -14,7 +16,7 @@ const logger = createLogger('core:TrayManager');
|
||||
*/
|
||||
export type TrayIdentifiers = 'main';
|
||||
|
||||
export default class TrayManager {
|
||||
export class TrayManager {
|
||||
app: App;
|
||||
|
||||
/**
|
||||
@@ -54,9 +56,13 @@ export default class TrayManager {
|
||||
initializeMainTray() {
|
||||
logger.debug('初始化主托盘');
|
||||
return this.retrieveOrInitialize({
|
||||
iconPath: 'tray-icon.png',
|
||||
identifier: 'main', // 使用应用图标,需要确保资源目录中有此文件
|
||||
tooltip: name, // 可以使用 app.getName() 或本地化字符串
|
||||
iconPath: isMac
|
||||
? nativeTheme.shouldUseDarkColors
|
||||
? 'tray-dark.png'
|
||||
: 'tray-light.png'
|
||||
: 'tray.png',
|
||||
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,164 @@
|
||||
[
|
||||
{
|
||||
"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."]
|
||||
},
|
||||
"date": "2025-07-22",
|
||||
"version": "1.102.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add notification for desktop."]
|
||||
},
|
||||
"date": "2025-07-22",
|
||||
"version": "1.102.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Modal list header sticky style."]
|
||||
},
|
||||
"date": "2025-07-21",
|
||||
"version": "1.102.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Add image generation capabilities using Google AI Imagen API."]
|
||||
},
|
||||
"date": "2025-07-21",
|
||||
"version": "1.102.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Fix lobehub provider /chat in desktop."]
|
||||
},
|
||||
"date": "2025-07-21",
|
||||
"version": "1.101.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Try fix authorization code exchange & pin next-auto to beta.29."]
|
||||
},
|
||||
"date": "2025-07-19",
|
||||
"version": "1.101.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Add zhipu cogview4."],
|
||||
"fixes": ["Some ai image bugs."]
|
||||
},
|
||||
"date": "2025-07-19",
|
||||
"version": "1.101.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix webapi proxy with clerk."]
|
||||
},
|
||||
"date": "2025-07-18",
|
||||
"version": "1.100.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Use server env config image models."]
|
||||
},
|
||||
"date": "2025-07-17",
|
||||
"version": "1.100.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Refactor desktop oauth and use JWTs token to support remote chat."]
|
||||
},
|
||||
"date": "2025-07-17",
|
||||
"version": "1.100.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Desktop local db can't upload image."]
|
||||
|
||||
@@ -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` 进行错误封装,保持错误信息的一致性
|
||||
@@ -625,4 +625,29 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
|
||||
- Default: `-`
|
||||
- Example: `-all,+qwq-32b,+deepseek-r1`
|
||||
|
||||
## FAL
|
||||
|
||||
### `ENABLED_FAL`
|
||||
|
||||
- Type: Optional
|
||||
- Description: Enables FAL as a model provider by default. Set to `0` to disable the FAL service.
|
||||
- Default: `1`
|
||||
- Example: `0`
|
||||
|
||||
### `FAL_API_KEY`
|
||||
|
||||
- Type: Required
|
||||
- Description: This is the API key you applied for in the FAL service.
|
||||
- Default: -
|
||||
- Example: `fal-xxxxxx...xxxxxx`
|
||||
|
||||
### `FAL_MODEL_LIST`
|
||||
|
||||
- Type: Optional
|
||||
- Description: Used to control the FAL model list. Use `+` to add a model, `-` to hide a model, and `model_name=display_name` to customize the display name of a model. Separate multiple entries with commas. The definition syntax follows the same rules as other providers' model lists.
|
||||
- Default: `-`
|
||||
- Example: `-all,+fal-model-1,+fal-model-2=fal-special`
|
||||
|
||||
The above example disables all models first, then enables `fal-model-1` and `fal-model-2` (displayed as `fal-special`).
|
||||
|
||||
[model-list]: /docs/self-hosting/advanced/model-list
|
||||
|
||||
@@ -624,4 +624,29 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
|
||||
- 默认值:`-`
|
||||
- 示例:`-all,+qwq-32b,+deepseek-r1`
|
||||
|
||||
## FAL
|
||||
|
||||
### `ENABLED_FAL`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:默认启用 FAL 作为模型供应商,当设为 0 时关闭 FAL 服务
|
||||
- 默认值:`1`
|
||||
- 示例:`0`
|
||||
|
||||
### `FAL_API_KEY`
|
||||
|
||||
- 类型:必选
|
||||
- 描述:这是你在 FAL 服务中申请的 API 密钥
|
||||
- 默认值:-
|
||||
- 示例:`fal-xxxxxx...xxxxxx`
|
||||
|
||||
### `FAL_MODEL_LIST`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:用来控制 FAL 模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则与其他 provider 保持一致。
|
||||
- 默认值:`-`
|
||||
- 示例:`-all,+fal-model-1,+fal-model-2=fal-special`
|
||||
|
||||
上述示例表示先禁用所有模型,再启用 `fal-model-1` 和 `fal-model-2`(显示名为 `fal-special`)。
|
||||
|
||||
[model-list]: /zh/docs/self-hosting/advanced/model-list
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Resolving AI Image Generation Timeout on Vercel
|
||||
description: >-
|
||||
Learn how to resolve timeout issues when using AI image generation models like gpt-image-1 on Vercel by enabling Fluid Compute for extended execution time.
|
||||
|
||||
tags:
|
||||
- Vercel
|
||||
- AI Image Generation
|
||||
- Timeout
|
||||
- Fluid Compute
|
||||
- gpt-image-1
|
||||
---
|
||||
|
||||
# Resolving AI Image Generation Timeout on Vercel
|
||||
|
||||
## Problem Description
|
||||
|
||||
When using AI image generation models (such as `gpt-image-1`) on Vercel, you may encounter timeout errors. This occurs because AI image generation typically requires more than 1 minute to complete, which exceeds Vercel's default function execution time limit.
|
||||
|
||||
Common error symptoms include:
|
||||
|
||||
- Function timeout errors during image generation
|
||||
- Failed image generation requests after approximately 60 seconds
|
||||
- "Function execution timed out" messages
|
||||
|
||||
### Typical Log Symptoms
|
||||
|
||||
In your Vercel function logs, you may see entries like this:
|
||||
|
||||
```plaintext
|
||||
JUL 16 18:39:09.51 POST 504 /trpc/async/image.createImage
|
||||
Provider runtime map found for provider: openai
|
||||
```
|
||||
|
||||
The key indicators are:
|
||||
|
||||
- **Status Code**: `504` (Gateway Timeout)
|
||||
- **Endpoint**: `/trpc/async/image.createImage` or similar image generation endpoints
|
||||
- **Timing**: Usually occurs around 60 seconds after the request starts
|
||||
|
||||
## Solution: Enable Fluid Compute
|
||||
|
||||
For projects created before Vercel's dashboard update, you can resolve this issue by enabling Fluid Compute, which extends the maximum execution duration to 300 seconds.
|
||||
|
||||
### Steps to Enable Fluid Compute (Legacy Vercel Dashboard)
|
||||
|
||||
1. Go to your project dashboard on Vercel
|
||||
2. Navigate to the **Settings** tab
|
||||
3. Find the **Functions** section
|
||||
4. Enable **Fluid Compute** as shown in the screenshot below:
|
||||
|
||||

|
||||
|
||||
5. After enabling, the maximum execution duration will be extended to 300 seconds by default
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **For new projects**: Newer Vercel projects have Fluid Compute enabled by default, so this issue primarily affects legacy projects
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information about Vercel's function limitations and Fluid Compute:
|
||||
|
||||
- [Vercel Fluid Compute Documentation](https://vercel.com/docs/fluid-compute)
|
||||
- [Vercel Functions Limitations](https://vercel.com/docs/functions/limitations#max-duration)
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 解决 Vercel 上 AI 绘画生图超时问题
|
||||
description: 了解如何通过开启 Fluid Compute 来解决在 Vercel 上使用 gpt-image-1 等 AI 绘画模型时遇到的超时问题。
|
||||
tags:
|
||||
- Vercel
|
||||
- AI 绘画
|
||||
- 超时问题
|
||||
- Fluid Compute
|
||||
- gpt-image-1
|
||||
---
|
||||
|
||||
# 解决 Vercel 上 AI 绘画生图超时问题
|
||||
|
||||
## 问题描述
|
||||
|
||||
在 Vercel 上使用 AI 绘画模型(如 `gpt-image-1`)时,您可能会遇到超时错误。这是因为 AI 绘画生成通常需要超过 1 分钟的时间,超出了 Vercel 默认的函数执行时间限制。
|
||||
|
||||
常见的错误症状包括:
|
||||
|
||||
- 图像生成过程中出现函数超时错误
|
||||
- 图像生成请求在大约 60 秒后失败
|
||||
- 出现 "函数执行超时" 的错误消息
|
||||
|
||||
### 典型的日志现象
|
||||
|
||||
在您的 Vercel 函数日志中,您可能会看到类似这样的条目:
|
||||
|
||||
```plaintext
|
||||
JUL 16 18:39:09.51 POST 504 /trpc/async/image.createImage
|
||||
Provider runtime map found for provider: openai
|
||||
```
|
||||
|
||||
关键指标包括:
|
||||
|
||||
- **状态码**: `504`(网关超时)
|
||||
- **端点**: `/trpc/async/image.createImage` 或类似的图像生成端点
|
||||
- **时间**: 通常在请求开始后约 60 秒出现
|
||||
|
||||
## 解决方案:开启 Fluid Compute
|
||||
|
||||
对于在 Vercel 控制台更新前创建的项目,您可以通过开启 Fluid Compute 来解决此问题,这将最大执行时长延长至 300 秒。
|
||||
|
||||
### 开启 Fluid Compute 的步骤(旧版 Vercel 控制台)
|
||||
|
||||
1. 前往您在 Vercel 上的项目控制台
|
||||
2. 进入 **Settings**(设置)选项卡
|
||||
3. 找到 **Functions**(函数)部分
|
||||
4. 按照下方截图所示开启 **Fluid Compute**:
|
||||
|
||||

|
||||
|
||||
5. 开启后,最大执行时长将默认延长至 300 秒
|
||||
|
||||
### 重要说明
|
||||
|
||||
- **新项目**:较新的 Vercel 项目默认已启用 Fluid Compute,因此此问题主要影响旧版项目
|
||||
|
||||
## 其他资源
|
||||
|
||||
有关 Vercel 函数限制和 Fluid Compute 的更多信息:
|
||||
|
||||
- [Vercel Fluid Compute 文档](https://vercel.com/docs/fluid-compute)
|
||||
- [Vercel 函数限制说明](https://vercel.com/docs/functions/limitations#max-duration)
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ tags:
|
||||
|
||||
# Using Fal in LobeChat
|
||||
|
||||
<Image alt={'Using Fal in LobeChat'} cover src={'https://github.com/user-attachments/assets/febb2ffb-8fe8-4f88-b8c9-8fa0985a2352'} />
|
||||
<Image alt={'Using Fal in LobeChat'} cover src={'https://hub-apac-1.lobeobjects.space/docs/f253e749baaa2ccac498014178f93091.png'} />
|
||||
|
||||
[Fal.ai](https://fal.ai/) is a lightning-fast inference platform specialized in AI media generation, hosting state-of-the-art models for image and video creation including FLUX, Kling, HiDream, and other cutting-edge generative models. This document will guide you on how to use Fal in LobeChat:
|
||||
|
||||
@@ -28,7 +28,7 @@ tags:
|
||||
alt={'Open the creation window'}
|
||||
inStep
|
||||
src={
|
||||
'https://github.com/user-attachments/assets/a2203b3a-1657-485a-a060-b018e7b2faaa'
|
||||
'https://hub-apac-1.lobeobjects.space/docs/3f3676e7f9c04a55603bc1174b636b45.png'
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@ tags:
|
||||
alt={'Create API Key'}
|
||||
inStep
|
||||
src={
|
||||
'https://github.com/user-attachments/assets/a216e326-6a51-4f3a-b8c1-23bb995ddac2'
|
||||
'https://hub-apac-1.lobeobjects.space/docs/214cc5019d9c0810951b33215349136e.png'
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -44,7 +44,7 @@ tags:
|
||||
alt={'Retrieve API Key'}
|
||||
inStep
|
||||
src={
|
||||
'https://github.com/user-attachments/assets/faee998d-4349-4c17-a5c4-07a7ff65f18e'
|
||||
'https://hub-apac-1.lobeobjects.space/docs/499a447e98dcc79407d56495d0305e2a.png'
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -53,12 +53,12 @@ tags:
|
||||
- Visit the `Settings` page in LobeChat.
|
||||
- Under **AI Service Provider**, locate the **Fal** configuration section.
|
||||
|
||||
<Image alt={'Enter API Key'} inStep src={'https://github.com/user-attachments/assets/7566f679-f935-4a5b-9c98-a8d99d9c7994'} />
|
||||
<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://github.com/user-attachments/assets/817167ff-4723-4f84-8528-c779c5c3a118'} />
|
||||
<Image alt={'Select Fal model for media generation'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/7560502f31b8500032922103fc22e69b.png'} />
|
||||
|
||||
<Callout type={'warning'}>
|
||||
During usage, you may incur charges according to Fal's pricing policy. Please review Fal's
|
||||
|
||||
@@ -13,7 +13,7 @@ tags:
|
||||
|
||||
# 在 LobeChat 中使用 Fal
|
||||
|
||||
<Image alt={'在 LobeChat 中使用 Fal'} cover src={'https://github.com/user-attachments/assets/febb2ffb-8fe8-4f88-b8c9-8fa0985a2352'} />
|
||||
<Image alt={'在 LobeChat 中使用 Fal'} cover src={'https://hub-apac-1.lobeobjects.space/docs/f253e749baaa2ccac498014178f93091.png'} />
|
||||
|
||||
[Fal.ai](https://fal.ai/) 是一个专门从事 AI 媒体生成的快速推理平台,提供包括 FLUX、Kling、HiDream 等在内的最先进图像和视频生成模型。本文将指导你如何在 LobeChat 中使用 Fal:
|
||||
|
||||
@@ -28,7 +28,7 @@ tags:
|
||||
alt={'打开创建窗口'}
|
||||
inStep
|
||||
src={
|
||||
'https://github.com/user-attachments/assets/a2203b3a-1657-485a-a060-b018e7b2faaa'
|
||||
'https://hub-apac-1.lobeobjects.space/docs/3f3676e7f9c04a55603bc1174b636b45.png'
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@ tags:
|
||||
alt={'创建 API Key'}
|
||||
inStep
|
||||
src={
|
||||
'https://github.com/user-attachments/assets/a216e326-6a51-4f3a-b8c1-23bb995ddac2'
|
||||
'https://hub-apac-1.lobeobjects.space/docs/214cc5019d9c0810951b33215349136e.png'
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -44,7 +44,7 @@ tags:
|
||||
alt={'获取 API Key'}
|
||||
inStep
|
||||
src={
|
||||
'https://github.com/user-attachments/assets/faee998d-4349-4c17-a5c4-07a7ff65f18e'
|
||||
'https://hub-apac-1.lobeobjects.space/docs/499a447e98dcc79407d56495d0305e2a.png'
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -53,12 +53,12 @@ tags:
|
||||
- 访问 LobeChat 的 `设置` 页面;
|
||||
- 在 `AI服务商` 下找到 `Fal` 的设置项;
|
||||
|
||||
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/user-attachments/assets/7566f679-f935-4a5b-9c98-a8d99d9c7994'} />
|
||||
<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://github.com/user-attachments/assets/817167ff-4723-4f84-8528-c779c5c3a118'} />
|
||||
<Image alt={'选择 Fal 模型进行媒体生成'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/7560502f31b8500032922103fc22e69b.png'} />
|
||||
|
||||
<Callout type={'warning'}>
|
||||
在使用过程中,你可能需要向 Fal 支付相应费用,请在大量调用前查阅 Fal 的官方计费政策。
|
||||
|
||||
@@ -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": "الإحصائيات"
|
||||
|
||||
@@ -70,11 +70,12 @@
|
||||
},
|
||||
"ImageUpload": {
|
||||
"actions": {
|
||||
"changeImage": "انقر لتغيير الصورة"
|
||||
"changeImage": "انقر لتغيير الصورة",
|
||||
"dropMultipleFiles": "لا يدعم تحميل ملفات متعددة في آن واحد، سيتم استخدام الملف الأول فقط"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "إضافة صورة",
|
||||
"secondary": "انقر للتحميل"
|
||||
"secondary": "انقر أو اسحب للإرفاق"
|
||||
}
|
||||
},
|
||||
"KeyValueEditor": {
|
||||
@@ -109,7 +110,7 @@
|
||||
},
|
||||
"MultiImagesUpload": {
|
||||
"actions": {
|
||||
"uploadMore": "انقر لتحميل المزيد"
|
||||
"uploadMore": "انقر أو اسحب لإضافة المزيد"
|
||||
},
|
||||
"modal": {
|
||||
"complete": "اكتمل",
|
||||
@@ -119,7 +120,7 @@
|
||||
"upload": "تحميل الصور"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "انقر لتحميل الصور",
|
||||
"primary": "انقر أو اسحب لتحميل الصور",
|
||||
"secondary": "يدعم اختيار عدة صور"
|
||||
},
|
||||
"progress": {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"notification": {
|
||||
"finishChatGeneration": "تم إنشاء رسالة الذكاء الاصطناعي بنجاح"
|
||||
},
|
||||
"proxy": {
|
||||
"auth": "يتطلب المصادقة",
|
||||
"authDesc": "إذا كان خادم الوكيل يتطلب اسم مستخدم وكلمة مرور",
|
||||
|
||||
@@ -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": "إعادة توليد الرسالة"
|
||||
|
||||
@@ -656,6 +656,9 @@
|
||||
"cognitivecomputations/dolphin-mixtral-8x22b": {
|
||||
"description": "Dolphin Mixtral 8x22B هو نموذج مصمم للامتثال للتعليمات، والحوار، والبرمجة."
|
||||
},
|
||||
"cogview-4": {
|
||||
"description": "CogView-4 هو أول نموذج مفتوح المصدر من Zhipu يدعم توليد الحروف الصينية، مع تحسينات شاملة في فهم المعاني، وجودة توليد الصور، وقدرات توليد النصوص باللغتين الصينية والإنجليزية، ويدعم إدخال ثنائي اللغة بأي طول، وقادر على توليد صور بأي دقة ضمن النطاق المحدد."
|
||||
},
|
||||
"cohere-command-r": {
|
||||
"description": "نموذج توليدي قابل للتوسع يستهدف RAG واستخدام الأدوات لتمكين الذكاء الاصطناعي على نطاق الإنتاج للمؤسسات."
|
||||
},
|
||||
@@ -1097,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، مصمم للاستخدام واسع النطاق."
|
||||
},
|
||||
@@ -1496,6 +1502,12 @@
|
||||
"hunyuan-vision": {
|
||||
"description": "نموذج Hunyuan الأحدث متعدد الوسائط، يدعم إدخال الصور والنصوص لتوليد محتوى نصي."
|
||||
},
|
||||
"imagen-4.0-generate-preview-06-06": {
|
||||
"description": "سلسلة نموذج Imagen للجيل الرابع لتحويل النص إلى صورة"
|
||||
},
|
||||
"imagen-4.0-ultra-generate-preview-06-06": {
|
||||
"description": "نسخة ألترا من سلسلة نموذج Imagen للجيل الرابع لتحويل النص إلى صورة"
|
||||
},
|
||||
"imagen4/preview": {
|
||||
"description": "نموذج توليد الصور الأعلى جودة من Google"
|
||||
},
|
||||
@@ -1916,6 +1928,9 @@
|
||||
"moonshotai/Kimi-Dev-72B": {
|
||||
"description": "Kimi-Dev-72B هو نموذج مفتوح المصدر للبرمجة، تم تحسينه عبر تعلم معزز واسع النطاق، قادر على إنتاج تصحيحات مستقرة وجاهزة للإنتاج مباشرة. حقق هذا النموذج نتيجة قياسية جديدة بنسبة 60.4% على SWE-bench Verified، محطماً الأرقام القياسية للنماذج المفتوحة المصدر في مهام هندسة البرمجيات الآلية مثل إصلاح العيوب ومراجعة الشيفرة."
|
||||
},
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
"description": "kimi-k2 هو نموذج أساسي مبني على بنية MoE يتمتع بقدرات فائقة في البرمجة والوكيل، مع إجمالي 1 تريليون معلمة و32 مليار معلمة مفعلة. في اختبارات الأداء المعيارية في مجالات المعرفة العامة، البرمجة، الرياضيات، والوكيل، يتفوق نموذج K2 على النماذج المفتوحة المصدر الرئيسية الأخرى."
|
||||
},
|
||||
"nousresearch/hermes-2-pro-llama-3-8b": {
|
||||
"description": "Hermes 2 Pro Llama 3 8B هو إصدار مطور من Nous Hermes 2، ويحتوي على أحدث مجموعات البيانات المطورة داخليًا."
|
||||
},
|
||||
@@ -2417,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> لتشفير مفتاحك وعنوان الوكيل",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"plans": {
|
||||
"plan": {
|
||||
"enterprise": {
|
||||
"title": "النسخة التجارية"
|
||||
},
|
||||
"free": {
|
||||
"title": "النسخة المجانية"
|
||||
},
|
||||
"hobby": {
|
||||
"title": "نسخة الخدمة الذاتية"
|
||||
},
|
||||
"premium": {
|
||||
"title": "النسخة المتقدمة"
|
||||
},
|
||||
"starter": {
|
||||
"title": "النسخة الأساسية"
|
||||
},
|
||||
"ultimate": {
|
||||
"title": "النسخة الاحترافية"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": "Статистика"
|
||||
|
||||
@@ -70,11 +70,12 @@
|
||||
},
|
||||
"ImageUpload": {
|
||||
"actions": {
|
||||
"changeImage": "Кликнете, за да смените изображението"
|
||||
"changeImage": "Кликнете, за да смените изображението",
|
||||
"dropMultipleFiles": "Не се поддържа качване на няколко файла едновременно, ще бъде използван само първият файл"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "Добавяне на изображение",
|
||||
"secondary": "Кликнете, за да качите"
|
||||
"secondary": "Кликнете или плъзнете, за да качите"
|
||||
}
|
||||
},
|
||||
"KeyValueEditor": {
|
||||
@@ -109,7 +110,7 @@
|
||||
},
|
||||
"MultiImagesUpload": {
|
||||
"actions": {
|
||||
"uploadMore": "Кликнете, за да качите още"
|
||||
"uploadMore": "Кликнете или плъзнете, за да качите още"
|
||||
},
|
||||
"modal": {
|
||||
"complete": "Готово",
|
||||
@@ -119,7 +120,7 @@
|
||||
"upload": "Качване на изображение"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "Кликнете, за да качите изображение",
|
||||
"primary": "Кликнете или плъзнете, за да качите изображение",
|
||||
"secondary": "Поддържа избор на няколко изображения"
|
||||
},
|
||||
"progress": {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"notification": {
|
||||
"finishChatGeneration": "Съобщението от AI е генерирано успешно"
|
||||
},
|
||||
"proxy": {
|
||||
"auth": "Необходимо удостоверяване",
|
||||
"authDesc": "Ако прокси сървърът изисква потребителско име и парола",
|
||||
|
||||
@@ -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": "Прегенериране на съобщение"
|
||||
|
||||
@@ -656,6 +656,9 @@
|
||||
"cognitivecomputations/dolphin-mixtral-8x22b": {
|
||||
"description": "Dolphin Mixtral 8x22B е модел, проектиран за следване на инструкции, диалози и програмиране."
|
||||
},
|
||||
"cogview-4": {
|
||||
"description": "CogView-4 е първият отворен модел за генериране на изображения с текст на китайски, разработен от Zhipu, който значително подобрява разбирането на семантиката, качеството на генериране на изображения и способността за генериране на текст на китайски и английски език. Поддържа двуезичен вход на произволна дължина на китайски и английски и може да генерира изображения с произволна резолюция в зададения диапазон."
|
||||
},
|
||||
"cohere-command-r": {
|
||||
"description": "Command R е мащабируем генеративен модел, насочен към RAG и използване на инструменти, за да позволи AI на производствено ниво за предприятия."
|
||||
},
|
||||
@@ -1097,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, проектиран за мащабна употреба."
|
||||
},
|
||||
@@ -1496,6 +1502,12 @@
|
||||
"hunyuan-vision": {
|
||||
"description": "Най-новият мултимодален модел на HunYuan, поддържащ генериране на текстово съдържание от изображения и текстови входове."
|
||||
},
|
||||
"imagen-4.0-generate-preview-06-06": {
|
||||
"description": "Imagen 4-то поколение текст-към-изображение модел серия"
|
||||
},
|
||||
"imagen-4.0-ultra-generate-preview-06-06": {
|
||||
"description": "Imagen 4-то поколение текст-към-изображение модел серия Ултра версия"
|
||||
},
|
||||
"imagen4/preview": {
|
||||
"description": "Най-висококачественият модел за генериране на изображения на Google."
|
||||
},
|
||||
@@ -1916,6 +1928,9 @@
|
||||
"moonshotai/Kimi-Dev-72B": {
|
||||
"description": "Kimi-Dev-72B е голям отворен модел за код, оптимизиран чрез мащабно подсилено обучение, способен да генерира стабилни и директно приложими пачове. Този модел постига нов рекорд от 60,4 % на SWE-bench Verified, подобрявайки резултатите на отворени модели в автоматизирани задачи за софтуерно инженерство като поправка на дефекти и преглед на код."
|
||||
},
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
"description": "kimi-k2 е базов модел с MoE архитектура с изключителни способности за кодиране и агент, с общо 1 трилион параметри и 32 милиарда активни параметри. В бенчмаркови тестове за общи знания, програмиране, математика и агенти, моделът K2 превъзхожда други водещи отворени модели."
|
||||
},
|
||||
"nousresearch/hermes-2-pro-llama-3-8b": {
|
||||
"description": "Hermes 2 Pro Llama 3 8B е обновена версия на Nous Hermes 2, включваща най-новите вътрешно разработени набори от данни."
|
||||
},
|
||||
@@ -2417,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>",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"plans": {
|
||||
"plan": {
|
||||
"enterprise": {
|
||||
"title": "Корпоративна версия"
|
||||
},
|
||||
"free": {
|
||||
"title": "Безплатна версия"
|
||||
},
|
||||
"hobby": {
|
||||
"title": "Самостоятелен вариант"
|
||||
},
|
||||
"premium": {
|
||||
"title": "Напреднал"
|
||||
},
|
||||
"starter": {
|
||||
"title": "Основна версия"
|
||||
},
|
||||
"ultimate": {
|
||||
"title": "Професионален"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -70,11 +70,12 @@
|
||||
},
|
||||
"ImageUpload": {
|
||||
"actions": {
|
||||
"changeImage": "Bild ändern"
|
||||
"changeImage": "Klicken, um das Bild zu ändern",
|
||||
"dropMultipleFiles": "Das Hochladen mehrerer Dateien wird nicht unterstützt, es wird nur die erste Datei verwendet"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "Bild hinzufügen",
|
||||
"secondary": "Zum Hochladen klicken"
|
||||
"secondary": "Klicken oder ziehen, um hochzuladen"
|
||||
}
|
||||
},
|
||||
"KeyValueEditor": {
|
||||
@@ -109,7 +110,7 @@
|
||||
},
|
||||
"MultiImagesUpload": {
|
||||
"actions": {
|
||||
"uploadMore": "Klicken Sie, um weitere hochzuladen"
|
||||
"uploadMore": "Klicken oder ziehen, um mehr hochzuladen"
|
||||
},
|
||||
"modal": {
|
||||
"complete": "Fertig",
|
||||
@@ -119,7 +120,7 @@
|
||||
"upload": "Bild hochladen"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "Klicken Sie, um Bilder hochzuladen",
|
||||
"primary": "Klicken oder ziehen, um Bilder hochzuladen",
|
||||
"secondary": "Mehrere Bilder können ausgewählt werden"
|
||||
},
|
||||
"progress": {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"notification": {
|
||||
"finishChatGeneration": "KI-Nachricht wurde vollständig generiert"
|
||||
},
|
||||
"proxy": {
|
||||
"auth": "Authentifizierung erforderlich",
|
||||
"authDesc": "Wenn der Proxy-Server Benutzername und Passwort benötigt",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -656,6 +656,9 @@
|
||||
"cognitivecomputations/dolphin-mixtral-8x22b": {
|
||||
"description": "Dolphin Mixtral 8x22B ist ein Modell, das für die Befolgung von Anweisungen, Dialoge und Programmierung entwickelt wurde."
|
||||
},
|
||||
"cogview-4": {
|
||||
"description": "CogView-4 ist das erste von Zhipu entwickelte Open-Source-Text-zu-Bild-Modell, das die Generierung chinesischer Schriftzeichen unterstützt. Es bietet umfassende Verbesserungen in den Bereichen semantisches Verständnis, Bildgenerierungsqualität und die Fähigkeit, chinesische und englische Schriftzeichen zu erzeugen. Es unterstützt mehrsprachige Eingaben beliebiger Länge in Chinesisch und Englisch und kann Bilder in beliebiger Auflösung innerhalb eines vorgegebenen Bereichs erzeugen."
|
||||
},
|
||||
"cohere-command-r": {
|
||||
"description": "Command R ist ein skalierbares generatives Modell, das auf RAG und Tool-Nutzung abzielt, um KI in Produktionsgröße für Unternehmen zu ermöglichen."
|
||||
},
|
||||
@@ -1097,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."
|
||||
},
|
||||
@@ -1496,6 +1502,12 @@
|
||||
"hunyuan-vision": {
|
||||
"description": "Das neueste multimodale Modell von Hunyuan unterstützt die Eingabe von Bildern und Text zur Generierung von Textinhalten."
|
||||
},
|
||||
"imagen-4.0-generate-preview-06-06": {
|
||||
"description": "Imagen 4. Generation Text-zu-Bild Modellserie"
|
||||
},
|
||||
"imagen-4.0-ultra-generate-preview-06-06": {
|
||||
"description": "Imagen 4. Generation Text-zu-Bild Modellserie Ultra-Version"
|
||||
},
|
||||
"imagen4/preview": {
|
||||
"description": "Googles hochwertigstes Bildgenerierungsmodell"
|
||||
},
|
||||
@@ -1916,6 +1928,9 @@
|
||||
"moonshotai/Kimi-Dev-72B": {
|
||||
"description": "Kimi-Dev-72B ist ein Open-Source-Großmodell für Quellcode, das durch umfangreiche Verstärkungslernoptimierung robuste und direkt produktionsreife Patches erzeugen kann. Dieses Modell erreichte auf SWE-bench Verified eine neue Höchstpunktzahl von 60,4 % und stellte damit einen Rekord für Open-Source-Modelle bei automatisierten Software-Engineering-Aufgaben wie Fehlerbehebung und Code-Review auf."
|
||||
},
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
"description": "kimi-k2 ist ein MoE-Architektur-Basismodell mit außergewöhnlichen Fähigkeiten in Code und Agenten, mit insgesamt 1 Billion Parametern und 32 Milliarden aktiven Parametern. In Benchmark-Tests zu allgemeinem Wissen, Programmierung, Mathematik und Agenten übertrifft das K2-Modell andere führende Open-Source-Modelle."
|
||||
},
|
||||
"nousresearch/hermes-2-pro-llama-3-8b": {
|
||||
"description": "Hermes 2 Pro Llama 3 8B ist die aktualisierte Version von Nous Hermes 2 und enthält die neuesten intern entwickelten Datensätze."
|
||||
},
|
||||
@@ -2417,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.",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"plans": {
|
||||
"plan": {
|
||||
"enterprise": {
|
||||
"title": "Unternehmensversion"
|
||||
},
|
||||
"free": {
|
||||
"title": "Kostenlose Version"
|
||||
},
|
||||
"hobby": {
|
||||
"title": "Selbstbedienungs-Version"
|
||||
},
|
||||
"premium": {
|
||||
"title": "Premium"
|
||||
},
|
||||
"starter": {
|
||||
"title": "Grundversion"
|
||||
},
|
||||
"ultimate": {
|
||||
"title": "Ultimate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -70,11 +70,12 @@
|
||||
},
|
||||
"ImageUpload": {
|
||||
"actions": {
|
||||
"changeImage": "Click to Change Image"
|
||||
"changeImage": "Click to change image",
|
||||
"dropMultipleFiles": "Uploading multiple files at once is not supported; only the first file will be used"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "Add Image",
|
||||
"secondary": "Click to Upload"
|
||||
"secondary": "Click or drag to upload"
|
||||
}
|
||||
},
|
||||
"KeyValueEditor": {
|
||||
@@ -109,7 +110,7 @@
|
||||
},
|
||||
"MultiImagesUpload": {
|
||||
"actions": {
|
||||
"uploadMore": "Click to upload more"
|
||||
"uploadMore": "Click or drag to upload more"
|
||||
},
|
||||
"modal": {
|
||||
"complete": "Done",
|
||||
@@ -119,7 +120,7 @@
|
||||
"upload": "Upload Images"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "Click to upload images",
|
||||
"primary": "Click or drag to upload images",
|
||||
"secondary": "Supports multiple image selection"
|
||||
},
|
||||
"progress": {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"notification": {
|
||||
"finishChatGeneration": "AI message generation completed"
|
||||
},
|
||||
"proxy": {
|
||||
"auth": "Authentication Required",
|
||||
"authDesc": "If the proxy server requires a username and password",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -656,6 +656,9 @@
|
||||
"cognitivecomputations/dolphin-mixtral-8x22b": {
|
||||
"description": "Dolphin Mixtral 8x22B is a model designed for instruction following, dialogue, and programming."
|
||||
},
|
||||
"cogview-4": {
|
||||
"description": "CogView-4 is Zhipu's first open-source text-to-image model supporting Chinese character generation. It offers comprehensive improvements in semantic understanding, image generation quality, and bilingual Chinese-English text generation capabilities. It supports bilingual input of any length and can generate images at any resolution within a specified range."
|
||||
},
|
||||
"cohere-command-r": {
|
||||
"description": "Command R is a scalable generative model targeting RAG and Tool Use to enable production-scale AI for enterprises."
|
||||
},
|
||||
@@ -1097,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."
|
||||
},
|
||||
@@ -1496,6 +1502,12 @@
|
||||
"hunyuan-vision": {
|
||||
"description": "The latest multimodal model from Hunyuan, supporting image + text input to generate textual content."
|
||||
},
|
||||
"imagen-4.0-generate-preview-06-06": {
|
||||
"description": "Imagen 4th generation text-to-image model series"
|
||||
},
|
||||
"imagen-4.0-ultra-generate-preview-06-06": {
|
||||
"description": "Imagen 4th generation text-to-image model series Ultra version"
|
||||
},
|
||||
"imagen4/preview": {
|
||||
"description": "Google's highest quality image generation model."
|
||||
},
|
||||
@@ -1916,6 +1928,9 @@
|
||||
"moonshotai/Kimi-Dev-72B": {
|
||||
"description": "Kimi-Dev-72B is an open-source large code model optimized through extensive reinforcement learning, capable of producing robust, production-ready patches. This model achieved a new high score of 60.4% on SWE-bench Verified, setting a record for open-source models in automated software engineering tasks such as defect repair and code review."
|
||||
},
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
"description": "kimi-k2 is a MoE architecture base model with powerful coding and Agent capabilities, featuring a total of 1 trillion parameters and 32 billion active parameters. In benchmark tests across key categories such as general knowledge reasoning, programming, mathematics, and Agent tasks, the K2 model outperforms other mainstream open-source models."
|
||||
},
|
||||
"nousresearch/hermes-2-pro-llama-3-8b": {
|
||||
"description": "Hermes 2 Pro Llama 3 8B is an upgraded version of Nous Hermes 2, featuring the latest internally developed datasets."
|
||||
},
|
||||
@@ -2417,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",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"plans": {
|
||||
"plan": {
|
||||
"enterprise": {
|
||||
"title": "Enterprise"
|
||||
},
|
||||
"free": {
|
||||
"title": "Free"
|
||||
},
|
||||
"hobby": {
|
||||
"title": "Hobby"
|
||||
},
|
||||
"premium": {
|
||||
"title": "Premium"
|
||||
},
|
||||
"starter": {
|
||||
"title": "Starter"
|
||||
},
|
||||
"ultimate": {
|
||||
"title": "Ultimate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -70,11 +70,12 @@
|
||||
},
|
||||
"ImageUpload": {
|
||||
"actions": {
|
||||
"changeImage": "Haz clic para cambiar la imagen"
|
||||
"changeImage": "Haz clic para cambiar la imagen",
|
||||
"dropMultipleFiles": "No se admite la carga múltiple de archivos, solo se utilizará el primer archivo"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "Agregar imagen",
|
||||
"secondary": "Haz clic para subir"
|
||||
"secondary": "Haga clic o arrastre para subir"
|
||||
}
|
||||
},
|
||||
"KeyValueEditor": {
|
||||
@@ -109,7 +110,7 @@
|
||||
},
|
||||
"MultiImagesUpload": {
|
||||
"actions": {
|
||||
"uploadMore": "Haz clic para subir más"
|
||||
"uploadMore": "Haz clic o arrastra para subir más"
|
||||
},
|
||||
"modal": {
|
||||
"complete": "Completar",
|
||||
@@ -119,7 +120,7 @@
|
||||
"upload": "Subir imágenes"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "Haz clic para subir imágenes",
|
||||
"primary": "Haz clic o arrastra para subir imágenes",
|
||||
"secondary": "Se admite la selección de múltiples imágenes"
|
||||
},
|
||||
"progress": {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"notification": {
|
||||
"finishChatGeneration": "El mensaje de IA se ha generado por completo"
|
||||
},
|
||||
"proxy": {
|
||||
"auth": "Se requiere autenticación",
|
||||
"authDesc": "Si el servidor proxy requiere nombre de usuario y contraseña",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -656,6 +656,9 @@
|
||||
"cognitivecomputations/dolphin-mixtral-8x22b": {
|
||||
"description": "Dolphin Mixtral 8x22B es un modelo diseñado para seguir instrucciones, diálogos y programación."
|
||||
},
|
||||
"cogview-4": {
|
||||
"description": "CogView-4 es el primer modelo de generación de imágenes a partir de texto de código abierto de Zhipu que admite la generación de caracteres chinos. Ofrece mejoras integrales en la comprensión semántica, la calidad de generación de imágenes y la capacidad de generar texto en chino e inglés. Soporta entradas bilingües en chino e inglés de cualquier longitud y puede generar imágenes en cualquier resolución dentro del rango especificado."
|
||||
},
|
||||
"cohere-command-r": {
|
||||
"description": "Command R es un modelo generativo escalable dirigido a RAG y uso de herramientas para habilitar IA a escala de producción para empresas."
|
||||
},
|
||||
@@ -1097,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."
|
||||
},
|
||||
@@ -1496,6 +1502,12 @@
|
||||
"hunyuan-vision": {
|
||||
"description": "El último modelo multimodal de Hunyuan, que admite la entrada de imágenes y texto para generar contenido textual."
|
||||
},
|
||||
"imagen-4.0-generate-preview-06-06": {
|
||||
"description": "Serie de modelos de texto a imagen de cuarta generación de Imagen"
|
||||
},
|
||||
"imagen-4.0-ultra-generate-preview-06-06": {
|
||||
"description": "Serie de modelos de texto a imagen de cuarta generación de Imagen, versión Ultra"
|
||||
},
|
||||
"imagen4/preview": {
|
||||
"description": "El modelo de generación de imágenes de mayor calidad de Google."
|
||||
},
|
||||
@@ -1916,6 +1928,9 @@
|
||||
"moonshotai/Kimi-Dev-72B": {
|
||||
"description": "Kimi-Dev-72B es un modelo de código abierto de gran escala, optimizado mediante aprendizaje reforzado a gran escala, capaz de generar parches robustos y listos para producción. Este modelo alcanzó un nuevo récord del 60.4 % en SWE-bench Verified, estableciendo un nuevo estándar para modelos de código abierto en tareas automatizadas de ingeniería de software como la corrección de errores y la revisión de código."
|
||||
},
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
"description": "kimi-k2 es un modelo base con arquitectura MoE que cuenta con capacidades avanzadas de código y agentes, con un total de 1T parámetros y 32B parámetros activados. En pruebas de referencia en categorías principales como razonamiento de conocimiento general, programación, matemáticas y agentes, el modelo K2 supera el rendimiento de otros modelos de código abierto populares."
|
||||
},
|
||||
"nousresearch/hermes-2-pro-llama-3-8b": {
|
||||
"description": "Hermes 2 Pro Llama 3 8B es una versión mejorada de Nous Hermes 2, que incluye los conjuntos de datos más recientes desarrollados internamente."
|
||||
},
|
||||
@@ -2417,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>",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"plans": {
|
||||
"plan": {
|
||||
"enterprise": {
|
||||
"title": "Versión empresarial"
|
||||
},
|
||||
"free": {
|
||||
"title": "Versión gratuita"
|
||||
},
|
||||
"hobby": {
|
||||
"title": "Versión de autoservicio"
|
||||
},
|
||||
"premium": {
|
||||
"title": "Premium"
|
||||
},
|
||||
"starter": {
|
||||
"title": "Versión básica"
|
||||
},
|
||||
"ultimate": {
|
||||
"title": "Ultimate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": "آمار"
|
||||
|
||||
@@ -70,11 +70,12 @@
|
||||
},
|
||||
"ImageUpload": {
|
||||
"actions": {
|
||||
"changeImage": "برای تغییر تصویر کلیک کنید"
|
||||
"changeImage": "برای تغییر تصویر کلیک کنید",
|
||||
"dropMultipleFiles": "بارگذاری چندین فایل به طور همزمان پشتیبانی نمیشود، فقط اولین فایل استفاده خواهد شد"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "افزودن تصویر",
|
||||
"secondary": "برای بارگذاری کلیک کنید"
|
||||
"secondary": "برای آپلود کلیک کنید یا بکشید"
|
||||
}
|
||||
},
|
||||
"KeyValueEditor": {
|
||||
@@ -109,7 +110,7 @@
|
||||
},
|
||||
"MultiImagesUpload": {
|
||||
"actions": {
|
||||
"uploadMore": "برای آپلود بیشتر کلیک کنید"
|
||||
"uploadMore": "برای آپلود بیشتر کلیک کنید یا بکشید"
|
||||
},
|
||||
"modal": {
|
||||
"complete": "تکمیل",
|
||||
@@ -119,7 +120,7 @@
|
||||
"upload": "آپلود تصویر"
|
||||
},
|
||||
"placeholder": {
|
||||
"primary": "برای آپلود تصویر کلیک کنید",
|
||||
"primary": "برای آپلود تصویر کلیک کنید یا بکشید",
|
||||
"secondary": "انتخاب چندین تصویر پشتیبانی میشود"
|
||||
},
|
||||
"progress": {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"notification": {
|
||||
"finishChatGeneration": "پیام هوش مصنوعی بهطور کامل تولید شد"
|
||||
},
|
||||
"proxy": {
|
||||
"auth": "نیاز به تأیید هویت",
|
||||
"authDesc": "اگر سرور پروکسی به نام کاربری و رمز عبور نیاز دارد",
|
||||
|
||||
@@ -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": "تولید مجدد پیام"
|
||||
|
||||
@@ -656,6 +656,9 @@
|
||||
"cognitivecomputations/dolphin-mixtral-8x22b": {
|
||||
"description": "Dolphin Mixtral 8x22B یک مدل طراحی شده برای پیروی از دستورات، مکالمه و برنامهنویسی است."
|
||||
},
|
||||
"cogview-4": {
|
||||
"description": "CogView-4 نخستین مدل متن به تصویر متنباز Zhizhu است که از تولید حروف چینی پشتیبانی میکند. این مدل در درک معنایی، کیفیت تولید تصویر و توانایی تولید متون چینی و انگلیسی به طور جامع بهبود یافته است، از ورودی دوزبانه چینی و انگلیسی با طول دلخواه پشتیبانی میکند و قادر است تصاویر با هر وضوحی در محدوده داده شده تولید کند."
|
||||
},
|
||||
"cohere-command-r": {
|
||||
"description": "Command R یک مدل تولیدی قابل گسترش است که برای RAG و استفاده از ابزارها طراحی شده است و به شرکتها امکان میدهد تا به هوش مصنوعی در سطح تولید دست یابند."
|
||||
},
|
||||
@@ -1097,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 کوچکترین و مقرونبهصرفهترین مدل گوگل است که برای استفاده در مقیاس بزرگ طراحی شده است."
|
||||
},
|
||||
@@ -1496,6 +1502,12 @@
|
||||
"hunyuan-vision": {
|
||||
"description": "جدیدترین مدل چندوجهی هونیوان، پشتیبانی از ورودی تصویر + متن برای تولید محتوای متنی."
|
||||
},
|
||||
"imagen-4.0-generate-preview-06-06": {
|
||||
"description": "سری مدل متن به تصویر نسل چهارم Imagen"
|
||||
},
|
||||
"imagen-4.0-ultra-generate-preview-06-06": {
|
||||
"description": "نسخه اولترا سری مدل متن به تصویر نسل چهارم Imagen"
|
||||
},
|
||||
"imagen4/preview": {
|
||||
"description": "مدل تولید تصویر با بالاترین کیفیت گوگل"
|
||||
},
|
||||
@@ -1916,6 +1928,9 @@
|
||||
"moonshotai/Kimi-Dev-72B": {
|
||||
"description": "Kimi-Dev-72B یک مدل بزرگ کد منبع باز است که با یادگیری تقویتی گسترده بهینه شده است و قادر به تولید پچهای پایدار و قابل استفاده مستقیم در تولید میباشد. این مدل در SWE-bench Verified امتیاز جدید ۶۰.۴٪ را کسب کرده و رکورد مدلهای منبع باز را در وظایف مهندسی نرمافزار خودکار مانند رفع اشکال و بازبینی کد شکسته است."
|
||||
},
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
"description": "kimi-k2 یک مدل پایه با معماری MoE است که دارای تواناییهای بسیار قوی در کدنویسی و عاملها میباشد، با مجموع پارامتر ۱ تریلیون و پارامترهای فعال ۳۲ میلیارد. در آزمونهای معیار عملکرد در دستههای اصلی مانند استدلال دانش عمومی، برنامهنویسی، ریاضیات و عاملها، مدل K2 عملکردی فراتر از سایر مدلهای متنباز رایج دارد."
|
||||
},
|
||||
"nousresearch/hermes-2-pro-llama-3-8b": {
|
||||
"description": "هرمس ۲ پرو لاما ۳ ۸B نسخه ارتقاء یافته Nous Hermes 2 است که شامل جدیدترین مجموعه دادههای توسعهیافته داخلی میباشد."
|
||||
},
|
||||
@@ -2417,6 +2432,9 @@
|
||||
"v0-1.5-md": {
|
||||
"description": "مدل v0-1.5-md برای وظایف روزمره و تولید رابط کاربری (UI) مناسب است"
|
||||
},
|
||||
"wanx2.1-t2i-turbo": {
|
||||
"description": "مدل تولید تصویر مبتنی بر متن زیرمجموعهی علیبابا کلود Tongyi"
|
||||
},
|
||||
"whisper-1": {
|
||||
"description": "مدل شناسایی گفتار عمومی که از شناسایی گفتار چندزبانه، ترجمه گفتار و شناسایی زبان پشتیبانی میکند."
|
||||
},
|
||||
|
||||