mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-17 21:08:36 +00:00
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7f4bb66e3 | |||
| c379be4461 | |||
| d8967c7e27 | |||
| dcb917fb23 | |||
| cea5e1417c | |||
| 37e32bcfce | |||
| 80eb4e3505 | |||
| 6463c4d12a | |||
| e65ae0ec2d | |||
| b7cd3fa010 | |||
| c444dca7f2 | |||
| 1025478de2 | |||
| 10141be2e5 | |||
| fed079ad0d | |||
| 0d10292989 | |||
| 9e19e439f7 | |||
| 96484851c0 | |||
| 2b72f28be3 | |||
| 29a08bb673 | |||
| 55c7c439f8 | |||
| 501f1a9938 | |||
| df580d79f8 | |||
| f4ea92399f | |||
| 649640a538 | |||
| a1a7673d79 | |||
| eefb14072e | |||
| b59577c2d2 | |||
| a89581216f | |||
| 6b7744e6a6 | |||
| acd0909cb2 | |||
| df71b12f1a | |||
| 5666e474dd | |||
| 20c5a8f80c | |||
| 9c094bc56c | |||
| c80e120328 | |||
| 1b0d02809c | |||
| 1a2055c0e6 | |||
| f15c3cb939 | |||
| 0078695da6 | |||
| 8b6b1eb995 | |||
| 2abccbb47a | |||
| 3b1b497d3e | |||
| 1267ed9f38 | |||
| bfbc80b7f7 | |||
| 4fe52294c0 | |||
| 5dedb3eda0 | |||
| 9eecc9becc | |||
| 28e1c5f091 | |||
| 9904fc41cc | |||
| ee86fd6058 | |||
| 2f0454719b | |||
| fa7ccaa353 | |||
| 90b1d19c77 | |||
| fcbdfffa6d | |||
| 3c68226bce | |||
| 7ed30fc881 | |||
| 80b230672b | |||
| 3508deff3e | |||
| 19b09e069f | |||
| 53b4b91af6 | |||
| 8719744225 | |||
| 120e01d8e7 | |||
| 443dd88446 | |||
| 65bae726c0 | |||
| 89b96b5e8e | |||
| 301908f377 | |||
| 19a9e88ffc | |||
| 5237e045ea | |||
| 35f19d9b31 | |||
| 7352b8a16b | |||
| 50e386b43f | |||
| 1bfe4579bb | |||
| 60ca998aac | |||
| 38dc1a69a4 | |||
| 3551ab8f64 | |||
| e82cb62109 | |||
| 5760f550ed | |||
| 4222768078 | |||
| e9bd57eeb3 | |||
| 7a003b0d37 | |||
| 26cf7d7308 | |||
| f9bb091f3a | |||
| aab42b4087 | |||
| f253881fc0 | |||
| 2b0e4aed44 | |||
| 23fc9c50d3 | |||
| 9bd27ea414 | |||
| 6f1cfa9480 | |||
| 55194e7986 | |||
| 2aa28fe8bc | |||
| 7001a2ea03 | |||
| b29581d69d | |||
| e2e70f1121 | |||
| cbd1d0f584 | |||
| f2745306af | |||
| d6e989d692 | |||
| de2f7f3a20 | |||
| 58adbdd983 | |||
| a762049b62 | |||
| 762127860d | |||
| 43c834d687 | |||
| 0a7ba6bf0b | |||
| dec483dccb | |||
| 0a9a8ab817 | |||
| d59694bb08 | |||
| 6088095119 | |||
| 75d591e779 | |||
| e1ddb27ef9 | |||
| b5a30c8359 | |||
| 20791df887 | |||
| e4bfeb00d0 | |||
| 2d3bd01b02 | |||
| 9f1f23125b | |||
| d6a69f9b5b | |||
| dd503ff418 | |||
| 882850f56b | |||
| 28a7796cec | |||
| f96e3bda5d | |||
| 9a241af65d | |||
| 2942f2244f | |||
| bc0a7a14d8 | |||
| e8314145e0 | |||
| dee6e5f97a | |||
| 031f8d143b | |||
| dc31d02bcd | |||
| 68394609b7 | |||
| a6263a45d2 | |||
| 62c6d12192 | |||
| 1c9c229e2a | |||
| 13fedf67f6 | |||
| 976d63b099 | |||
| 69966cb235 | |||
| d10ade40f9 | |||
| 7cbade75bd | |||
| 4c894a3b98 | |||
| d286c29745 | |||
| 561e80050f | |||
| 6c9b46e8df | |||
| 6a955cc3ea | |||
| 7d2ea6b243 | |||
| ee36f3210b | |||
| 4ab7013310 | |||
| 2243f82ba7 | |||
| 3656e162fe | |||
| 05eb57ec3c | |||
| d97138fab3 | |||
| af14b2fb04 | |||
| 3ecb3c4fe0 | |||
| e563be6a8c | |||
| 7bb6061a03 | |||
| fff64ea919 | |||
| 38da250e5d | |||
| 0b99552ada | |||
| e805e8cb96 | |||
| 37aabb7bd5 | |||
| 122cd6294c | |||
| 3a4d9ce33e | |||
| f291505215 | |||
| 79aab68167 | |||
| 356efab490 | |||
| 8079032563 | |||
| efc8d83221 | |||
| ff06a3c602 | |||
| 2ed15c50af | |||
| 1fe0b2e9f3 | |||
| 81400f3bdb | |||
| c14d2781dd | |||
| 46c8106185 | |||
| 26355231ec | |||
| 92a8fc1d38 | |||
| 425b773769 | |||
| 721b8c980d | |||
| aaf264ca0f | |||
| a807d658f3 | |||
| fdd63c25c7 | |||
| 4f88b498f7 | |||
| 2967f36805 | |||
| cfb2ced431 |
@@ -1,275 +0,0 @@
|
||||
# Agent Runtime E2E 测试指南
|
||||
|
||||
本文档描述 Agent Runtime 端到端测试的核心原则和实施方法。
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 最小化 Mock 原则
|
||||
|
||||
E2E 测试的目标是尽可能接近真实运行环境。因此,我们只 Mock **三个外部依赖**:
|
||||
|
||||
| 依赖 | Mock 方式 | 说明 |
|
||||
|------|----------|------|
|
||||
| **Database** | PGLite | 使用 `@lobechat/database/test-utils` 提供的内存数据库 |
|
||||
| **Redis** | InMemoryAgentStateManager | Mock `AgentStateManager` 使用内存实现 |
|
||||
| **Redis** | InMemoryStreamEventManager | Mock `StreamEventManager` 使用内存实现 |
|
||||
|
||||
**不 Mock 的部分:**
|
||||
|
||||
- `model-bank` - 使用真实的模型配置数据
|
||||
- `Mecha` (AgentToolsEngine, ContextEngineering) - 使用真实逻辑
|
||||
- `AgentRuntimeService` - 使用真实逻辑
|
||||
- `AgentRuntimeCoordinator` - 使用真实逻辑
|
||||
|
||||
### 2. 使用 vi.spyOn 而非 vi.mock
|
||||
|
||||
不同测试场景需要不同的 LLM 响应。使用 `vi.spyOn` 可以:
|
||||
|
||||
- 在每个测试中灵活控制返回值
|
||||
- 便于测试不同场景(纯文本、tool calls、错误等)
|
||||
- 避免全局 mock 导致的测试隔离问题
|
||||
|
||||
### 3. 默认模型使用 gpt-5
|
||||
|
||||
- `model-bank` 中肯定有该模型的数据
|
||||
- 避免短期内因模型更新需要修改测试
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 数据库设置
|
||||
|
||||
```typescript
|
||||
import { LobeChatDatabase } from '@lobechat/database';
|
||||
import { getTestDB } from '@lobechat/database/test-utils';
|
||||
|
||||
let testDB: LobeChatDatabase;
|
||||
|
||||
beforeEach(async () => {
|
||||
testDB = await getTestDB();
|
||||
});
|
||||
```
|
||||
|
||||
### OpenAI Response Mock Helper
|
||||
|
||||
创建一个 helper 函数来生成 OpenAI 格式的流式响应:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 创建 OpenAI 格式的流式响应
|
||||
*/
|
||||
export const createOpenAIStreamResponse = (options: {
|
||||
content?: string;
|
||||
toolCalls?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
}>;
|
||||
finishReason?: 'stop' | 'tool_calls';
|
||||
}) => {
|
||||
const { content, toolCalls, finishReason = 'stop' } = options;
|
||||
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// 发送内容 chunk
|
||||
if (content) {
|
||||
const chunk = {
|
||||
id: 'chatcmpl-mock',
|
||||
object: 'chat.completion.chunk',
|
||||
model: 'gpt-5',
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { content },
|
||||
finish_reason: null,
|
||||
}],
|
||||
};
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
||||
}
|
||||
|
||||
// 发送 tool_calls chunk
|
||||
if (toolCalls) {
|
||||
for (const tool of toolCalls) {
|
||||
const chunk = {
|
||||
id: 'chatcmpl-mock',
|
||||
object: 'chat.completion.chunk',
|
||||
model: 'gpt-5',
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: tool.id,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.name,
|
||||
arguments: tool.arguments,
|
||||
},
|
||||
}],
|
||||
},
|
||||
finish_reason: null,
|
||||
}],
|
||||
};
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
||||
}
|
||||
}
|
||||
|
||||
// 发送完成 chunk
|
||||
const finishChunk = {
|
||||
id: 'chatcmpl-mock',
|
||||
object: 'chat.completion.chunk',
|
||||
model: 'gpt-5',
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: {},
|
||||
finish_reason: finishReason,
|
||||
}],
|
||||
};
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
|
||||
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{ headers: { 'content-type': 'text/event-stream' } },
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 内存状态管理
|
||||
|
||||
使用依赖注入替代 Redis:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
InMemoryAgentStateManager,
|
||||
InMemoryStreamEventManager,
|
||||
} from '@/server/modules/AgentRuntime';
|
||||
import { AgentRuntimeService } from '@/server/services/agentRuntime';
|
||||
|
||||
const stateManager = new InMemoryAgentStateManager();
|
||||
const streamEventManager = new InMemoryStreamEventManager();
|
||||
|
||||
const service = new AgentRuntimeService(serverDB, userId, {
|
||||
coordinatorOptions: {
|
||||
stateManager,
|
||||
streamEventManager,
|
||||
},
|
||||
queueService: null, // 禁用 QStash 队列,使用 executeSync
|
||||
streamEventManager,
|
||||
});
|
||||
```
|
||||
|
||||
### Mock OpenAI API
|
||||
|
||||
在测试中使用 `vi.spyOn` mock fetch:
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// 在测试文件顶部或 beforeEach 中
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
||||
|
||||
// 在具体测试中设置返回值
|
||||
it('should handle text response', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({ content: '杭州今天天气晴朗' })
|
||||
);
|
||||
|
||||
// ... 执行测试
|
||||
});
|
||||
|
||||
it('should handle tool calls', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({
|
||||
toolCalls: [{
|
||||
id: 'call_123',
|
||||
name: 'lobe-web-browsing____search____builtin',
|
||||
arguments: JSON.stringify({ query: '杭州天气' }),
|
||||
}],
|
||||
finishReason: 'tool_calls',
|
||||
})
|
||||
);
|
||||
|
||||
// ... 执行测试
|
||||
});
|
||||
```
|
||||
|
||||
## 测试场景
|
||||
|
||||
### 1. 基本对话测试
|
||||
|
||||
```typescript
|
||||
describe('Basic Chat', () => {
|
||||
it('should complete a simple conversation', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({ content: 'Hello! How can I help you?' })
|
||||
);
|
||||
|
||||
const result = await service.createOperation({
|
||||
agentConfig: { model: 'gpt-5', provider: 'openai' },
|
||||
initialMessages: [{ role: 'user', content: 'Hi' }],
|
||||
// ...
|
||||
});
|
||||
|
||||
const finalState = await service.executeSync(result.operationId);
|
||||
expect(finalState.status).toBe('done');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Tool 调用测试
|
||||
|
||||
```typescript
|
||||
describe('Tool Calls', () => {
|
||||
it('should execute web-browsing tool', async () => {
|
||||
// 第一次调用:LLM 返回 tool_calls
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({
|
||||
toolCalls: [{
|
||||
id: 'call_123',
|
||||
name: 'lobe-web-browsing____search____builtin',
|
||||
arguments: JSON.stringify({ query: '杭州天气' }),
|
||||
}],
|
||||
finishReason: 'tool_calls',
|
||||
})
|
||||
);
|
||||
|
||||
// 第二次调用:处理 tool 结果后的响应
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({ content: '根据搜索结果,杭州今天...' })
|
||||
);
|
||||
|
||||
// ... 执行测试
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 错误处理测试
|
||||
|
||||
```typescript
|
||||
describe('Error Handling', () => {
|
||||
it('should handle API errors gracefully', async () => {
|
||||
fetchSpy.mockRejectedValueOnce(new Error('API rate limit exceeded'));
|
||||
|
||||
// ... 执行测试并验证错误处理
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 文件组织
|
||||
|
||||
```
|
||||
src/server/routers/lambda/__tests__/integration/
|
||||
├── setup.ts # 测试设置工具
|
||||
├── aiAgent.integration.test.ts # 现有集成测试
|
||||
├── aiAgent.e2e.test.ts # E2E 测试
|
||||
└── helpers/
|
||||
└── openaiMock.ts # OpenAI mock helper
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **测试隔离**:每个测试后清理 `InMemoryAgentStateManager` 和 `InMemoryStreamEventManager`
|
||||
2. **超时设置**:E2E 测试可能需要更长的超时时间
|
||||
3. **调试**:使用 `DEBUG=lobe-server:*` 环境变量查看详细日志
|
||||
@@ -2,58 +2,6 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [Version 2.0.0-next.164](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.163...v2.0.0-next.164)
|
||||
|
||||
<sup>Released on **2025-12-08**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **profile**: Add mobile responsive layout and signup improvements.
|
||||
- **misc**: Update link handling in PlanTag component to use react-router-dom.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **profile**: Add mobile responsive layout and signup improvements, closes [#10669](https://github.com/lobehub/lobe-chat/issues/10669) ([1afd471](https://github.com/lobehub/lobe-chat/commit/1afd471))
|
||||
- **misc**: Update link handling in PlanTag component to use react-router-dom, closes [#10673](https://github.com/lobehub/lobe-chat/issues/10673) ([3aceeb6](https://github.com/lobehub/lobe-chat/commit/3aceeb6))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 2.0.0-next.163](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.162...v2.0.0-next.163)
|
||||
|
||||
<sup>Released on **2025-12-06**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Add smooth scroll to top on 'More' button click in Title component.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Add smooth scroll to top on 'More' button click in Title component, closes [#10178](https://github.com/lobehub/lobe-chat/issues/10178) ([5ad4f0c](https://github.com/lobehub/lobe-chat/commit/5ad4f0c))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 2.0.0-next.162](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.161...v2.0.0-next.162)
|
||||
|
||||
<sup>Released on **2025-12-05**</sup>
|
||||
|
||||
@@ -44,8 +44,6 @@ see @.cursor/rules/typescript.mdc
|
||||
- wrap the file path in single quotes to avoid shell expansion
|
||||
- Never run `bun run test` etc to run tests, this will run all tests and cost about 10mins
|
||||
- If trying to fix the same test twice, but still failed, stop and ask for help.
|
||||
- **Prefer `vi.spyOn` over `vi.mock`**: When mocking modules or functions, prefer using `vi.spyOn` to mock specific functions rather than `vi.mock` to mock entire modules. This approach is more targeted, easier to maintain, and allows for better control over mock behavior in individual tests.
|
||||
- **Tests must pass type check**: After writing or modifying tests, run `bun run type-check` to ensure there are no type errors. Tests should pass both runtime execution and TypeScript type checking.
|
||||
|
||||
### Typecheck
|
||||
|
||||
|
||||
@@ -345,12 +345,12 @@ In addition, these plugins are not limited to news aggregation, but can also ext
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
| Recent Submits | Description |
|
||||
| --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [AladinBooks](https://lobechat.com/discover/plugin/AladinSearchBooks)<br/><sup>By **azurewebsites** on **2025-12-08**</sup> | Search for books on Aladin.<br/>`book` `search` |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
|
||||
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | Enter any URL and keyword and get an On-Page SEO analysis & insights!<br/>`seo` |
|
||||
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
|
||||
| Recent Submits | Description |
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
|
||||
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | Enter any URL and keyword and get an On-Page SEO analysis & insights!<br/>`seo` |
|
||||
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
|
||||
| [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` |
|
||||
|
||||
> 📊 Total plugins: [<kbd>**41**</kbd>](https://lobechat.com/discover/plugins)
|
||||
|
||||
|
||||
+6
-6
@@ -338,12 +338,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
| 最近新增 | 描述 |
|
||||
| --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| [AladinBooks](https://lobechat.com/discover/plugin/AladinSearchBooks)<br/><sup>By **azurewebsites** on **2025-12-08**</sup> | 在阿拉丁上搜索书籍。<br/>`书籍` `搜索` |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
|
||||
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | 输入任何 URL 和关键词,获取页面 SEO 分析和见解!<br/>`seo` |
|
||||
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
|
||||
| 最近新增 | 描述 |
|
||||
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
|
||||
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | 输入任何 URL 和关键词,获取页面 SEO 分析和见解!<br/>`seo` |
|
||||
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
|
||||
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
|
||||
|
||||
> 📊 Total plugins: [<kbd>**41**</kbd>](https://lobechat.com/discover/plugins)
|
||||
|
||||
|
||||
+38
-42
@@ -156,24 +156,26 @@ apps/desktop/src/main/
|
||||
- 事件广播:向渲染进程通知授权状态变化
|
||||
|
||||
```typescript
|
||||
// 认证流程示例
|
||||
@ipcClientEvent('requestAuthorization')
|
||||
async requestAuthorization(config: DataSyncConfig) {
|
||||
// 生成状态参数防止 CSRF 攻击
|
||||
this.authRequestState = crypto.randomBytes(16).toString('hex');
|
||||
import { ControllerModule, IpcMethod } from '@/controllers'
|
||||
|
||||
// 构建授权 URL
|
||||
const authUrl = new URL('/oidc/auth', remoteUrl);
|
||||
authUrl.search = querystring.stringify({
|
||||
client_id: 'lobe-chat',
|
||||
response_type: 'code',
|
||||
redirect_uri: `${protocolPrefix}://auth/callback`,
|
||||
scope: 'openid profile',
|
||||
state: this.authRequestState,
|
||||
});
|
||||
export default class AuthCtr extends ControllerModule {
|
||||
static override groupName = 'auth'
|
||||
|
||||
// 在默认浏览器中打开授权 URL
|
||||
await shell.openExternal(authUrl.toString());
|
||||
@IpcMethod()
|
||||
async requestAuthorization(config: DataSyncConfig) {
|
||||
this.authRequestState = crypto.randomBytes(16).toString('hex')
|
||||
|
||||
const authUrl = new URL('/oidc/auth', remoteUrl)
|
||||
authUrl.search = querystring.stringify({
|
||||
client_id: 'lobe-chat',
|
||||
redirect_uri: `${protocolPrefix}://auth/callback`,
|
||||
response_type: 'code',
|
||||
scope: 'openid profile',
|
||||
state: this.authRequestState,
|
||||
})
|
||||
|
||||
await shell.openExternal(authUrl.toString())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -267,20 +269,27 @@ export class ShortcutManager {
|
||||
- 注入 App 实例
|
||||
|
||||
```typescript
|
||||
// 控制器基类和装饰器
|
||||
import { ControllerModule, IpcMethod, IpcServerMethod } from '@/controllers'
|
||||
|
||||
export class ControllerModule implements IControllerModule {
|
||||
constructor(public app: App) {
|
||||
this.app = app;
|
||||
this.app = app
|
||||
}
|
||||
}
|
||||
|
||||
// IPC 客户端事件装饰器
|
||||
export const ipcClientEvent = (method: keyof ClientDispatchEvents) =>
|
||||
ipcDecorator(method, 'client');
|
||||
export class BrowserWindowsCtr extends ControllerModule {
|
||||
static override groupName = 'windows'
|
||||
|
||||
// IPC 服务器事件装饰器
|
||||
export const ipcServerEvent = (method: keyof ServerDispatchEvents) =>
|
||||
ipcDecorator(method, 'server');
|
||||
@IpcMethod()
|
||||
openSettingsWindow(params?: OpenSettingsWindowOptions) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@IpcServerMethod()
|
||||
handleServerCommand(payload: any) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **IoC 容器**:
|
||||
@@ -346,26 +355,13 @@ makeSureDirExist(storagePath);
|
||||
- 自动映射控制器方法到 IPC 事件
|
||||
|
||||
```typescript
|
||||
// IPC 事件初始化
|
||||
private initializeIPCEvents() {
|
||||
// 注册客户端事件处理程序
|
||||
this.ipcClientEventMap.forEach((eventInfo, key) => {
|
||||
ipcMain.handle(key, async (e, ...data) => {
|
||||
return await eventInfo.controller[eventInfo.methodName](...data);
|
||||
});
|
||||
});
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc'
|
||||
|
||||
// 注册服务器事件处理程序
|
||||
const ipcServerEvents = {} as ElectronIPCEventHandler;
|
||||
this.ipcServerEventMap.forEach((eventInfo, key) => {
|
||||
ipcServerEvents[key] = async (payload) => {
|
||||
return await eventInfo.controller[eventInfo.methodName](payload);
|
||||
};
|
||||
});
|
||||
// 渲染进程中使用 type-safe proxy 调用主进程方法
|
||||
const ipc = ensureElectronIpc()
|
||||
|
||||
// 创建 IPC 服务器
|
||||
this.ipcServer = new ElectronIPCServer(name, ipcServerEvents);
|
||||
}
|
||||
await ipc.localSystem.readLocalFile({ path })
|
||||
await ipc.system.updateLocale('en-US')
|
||||
```
|
||||
|
||||
2. **事件广播**:
|
||||
|
||||
+37
-1
@@ -183,10 +183,18 @@ The `App.ts` class orchestrates the entire application lifecycle through key pha
|
||||
#### 🔌 Dependency Injection & Event System
|
||||
|
||||
- **IoC Container** - WeakMap-based container for decorated controller methods
|
||||
- **Decorator Registration** - `@ipcClientEvent` and `@ipcServerEvent` decorators
|
||||
- **Typed IPC Decorators** - `@IpcMethod` and `@IpcServerMethod` wire controller methods into type-safe channels
|
||||
- **Automatic Event Mapping** - Events registered during controller loading
|
||||
- **Service Locator** - Type-safe service and controller retrieval
|
||||
|
||||
##### 🧠 Type-Safe IPC Flow
|
||||
|
||||
- **Async Context Propagation** - `src/main/utils/ipc/base.ts` captures the `IpcContext` with `AsyncLocalStorage`, so controller logic can call `getIpcContext()` anywhere inside an IPC handler without explicitly threading arguments.
|
||||
- **Service Constructors Registry** - `src/main/controllers/registry.ts` exports `controllerIpcConstructors`, `DesktopIpcServices`, and `DesktopServerIpcServices`, enabling automatic typing of both renderer and server IPC proxies.
|
||||
- **Renderer Proxy Helper** - `src/utils/electron/ipc.ts` exposes `ensureElectronIpc()` which lazily builds a proxy on top of `window.electronAPI.invoke`, giving React/Next.js code a type-safe API surface without exposing raw proxies in preload.
|
||||
- **Server Proxy Helper** - `src/server/modules/ElectronIPCClient/index.ts` mirrors the same typing strategy for the Next.js server runtime, providing a dedicated proxy for `@IpcServerMethod` handlers.
|
||||
- **Shared Typings Package** - `apps/desktop/src/main/exports.d.ts` augments `@lobechat/electron-client-ipc` so every package can consume `DesktopIpcServices` without importing desktop business code directly.
|
||||
|
||||
#### 🪟 Window Management
|
||||
|
||||
- **Theme-Aware Windows** - Automatic adaptation to system dark/light mode
|
||||
@@ -235,6 +243,7 @@ The `App.ts` class orchestrates the entire application lifecycle through key pha
|
||||
|
||||
#### 🎮 Controller Pattern
|
||||
|
||||
- **Typed IPC Decorators** - Controllers extend `ControllerModule` and expose renderer methods via `@IpcMethod`
|
||||
- **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
|
||||
@@ -256,6 +265,33 @@ The `App.ts` class orchestrates the entire application lifecycle through key pha
|
||||
- **Context Awareness** - Events include sender context for window-specific operations
|
||||
- **Error Propagation** - Centralized error handling with proper status codes
|
||||
|
||||
##### 🧩 Renderer IPC Helper
|
||||
|
||||
Renderer code uses a lightweight proxy generated at runtime to keep IPC calls type-safe without exposing raw Electron objects through `contextBridge`. Use the helper exported from `src/utils/electron/ipc.ts` to access the main-process services:
|
||||
|
||||
```ts
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
||||
|
||||
const ipc = ensureElectronIpc();
|
||||
await ipc.windows.openSettingsWindow({ tab: 'provider' });
|
||||
```
|
||||
|
||||
The helper internally builds a proxy on top of `window.electronAPI.invoke`, so no proxy objects need to be cloned across the preload boundary.
|
||||
|
||||
##### 🖥️ Server IPC Helper
|
||||
|
||||
Next.js (Node) modules use the same proxy pattern via `ensureElectronServerIpc` from `src/server/modules/ElectronIPCClient`. It lazily wraps the socket-based `ElectronIpcClient` so server code can call controllers with full type safety:
|
||||
|
||||
```ts
|
||||
import { ensureElectronServerIpc } from '@/server/modules/ElectronIPCClient';
|
||||
|
||||
const ipc = ensureElectronServerIpc();
|
||||
const dbPath = await ipc.system.getDatabasePath();
|
||||
await ipc.upload.deleteFiles(['foo.txt']);
|
||||
```
|
||||
|
||||
All server methods are declared via `@IpcServerMethod` and live in dedicated controller classes, keeping renderer typings clean.
|
||||
|
||||
#### 🛡️ Security Features
|
||||
|
||||
- **OAuth 2.0 + PKCE** - Secure authentication with state parameter validation
|
||||
|
||||
@@ -183,7 +183,7 @@ src/main/core/
|
||||
#### 🔌 依赖注入和事件系统
|
||||
|
||||
- **IoC 容器** - 基于 WeakMap 的装饰控制器方法容器
|
||||
- **装饰器注册** - `@ipcClientEvent` 和 `@ipcServerEvent` 装饰器
|
||||
- **装饰器注册** - `@IpcMethod` 和 `@IpcServerMethod` 装饰器
|
||||
- **自动事件映射** - 控制器加载期间注册的事件
|
||||
- **服务定位器** - 类型安全的服务和控制器检索
|
||||
|
||||
@@ -256,6 +256,31 @@ src/main/core/
|
||||
- **上下文感知** - 事件包含用于窗口特定操作的发送者上下文
|
||||
- **错误传播** - 具有适当状态码的集中错误处理
|
||||
|
||||
##### 🧩 渲染器 IPC 助手
|
||||
|
||||
渲染端通过 `src/utils/electron/ipc.ts` 提供的 `ensureElectronIpc` 获得一个运行时代理,无需在 preload 中暴露 Proxy 对象即可获得类型安全的调用体验:
|
||||
|
||||
```ts
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc'
|
||||
|
||||
const ipc = ensureElectronIpc()
|
||||
await ipc.windows.openSettingsWindow({ tab: 'provider' })
|
||||
```
|
||||
|
||||
##### 🖥️ Server IPC 助手
|
||||
|
||||
Next.js 服务端模块可通过 `ensureElectronServerIpc`(位于 `src/server/modules/ElectronIPCClient`)获得同样的类型安全代理,并复用 socket IPC 通道:
|
||||
|
||||
```ts
|
||||
import { ensureElectronServerIpc } from '@/server/modules/ElectronIPCClient'
|
||||
|
||||
const ipc = ensureElectronServerIpc()
|
||||
const path = await ipc.system.getDatabasePath()
|
||||
await ipc.upload.deleteFiles(['foo.txt'])
|
||||
```
|
||||
|
||||
所有 `@IpcServerMethod` 方法都放在独立的控制器中,这样渲染端的类型推导不会包含这些仅供服务器调用的通道。
|
||||
|
||||
#### 🛡️ 安全功能
|
||||
|
||||
- **OAuth 2.0 + PKCE** - 具有状态参数验证的安全认证
|
||||
|
||||
@@ -39,6 +39,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'~common': resolve(__dirname, 'src/common'),
|
||||
'@': resolve(__dirname, 'src/main'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import { URL } from 'node:url';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
// Create logger
|
||||
const logger = createLogger('controllers:AuthCtr');
|
||||
@@ -17,6 +17,7 @@ const logger = createLogger('controllers:AuthCtr');
|
||||
* Implements OAuth authorization flow using intermediate page + polling mechanism
|
||||
*/
|
||||
export default class AuthCtr extends ControllerModule {
|
||||
static override readonly groupName = 'auth';
|
||||
/**
|
||||
* Remote server configuration controller
|
||||
*/
|
||||
@@ -56,7 +57,7 @@ export default class AuthCtr extends ControllerModule {
|
||||
/**
|
||||
* Request OAuth authorization
|
||||
*/
|
||||
@ipcClientEvent('requestAuthorization')
|
||||
@IpcMethod()
|
||||
async requestAuthorization(config: DataSyncConfig) {
|
||||
// Clear any old authorization state
|
||||
this.clearAuthorizationState();
|
||||
@@ -119,7 +120,7 @@ export default class AuthCtr extends ControllerModule {
|
||||
/**
|
||||
* Request Market OAuth authorization (desktop)
|
||||
*/
|
||||
@ipcClientEvent('requestMarketAuthorization')
|
||||
@IpcMethod()
|
||||
async requestMarketAuthorization(params: MarketAuthorizationParams) {
|
||||
const { authUrl } = params;
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { InterceptRouteParams, OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc';
|
||||
import { findMatchingRoute } from '~common/routes';
|
||||
|
||||
import {
|
||||
AppBrowsersIdentifiers,
|
||||
WindowTemplateIdentifiers,
|
||||
} from '@/appBrowsers';
|
||||
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
||||
import { AppBrowsersIdentifiers, WindowTemplateIdentifiers } from '@/appBrowsers';
|
||||
import { getIpcContext } from '@/utils/ipc';
|
||||
|
||||
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
||||
import { ControllerModule, IpcMethod, shortcut } from './index';
|
||||
|
||||
export default class BrowserWindowsCtr extends ControllerModule {
|
||||
static override readonly groupName = 'windows';
|
||||
|
||||
@shortcut('showApp')
|
||||
async toggleMainWindow() {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.toggleVisible();
|
||||
}
|
||||
|
||||
@ipcClientEvent('openSettingsWindow')
|
||||
@IpcMethod()
|
||||
async openSettingsWindow(options?: string | OpenSettingsWindowOptions) {
|
||||
const normalizedOptions: OpenSettingsWindowOptions =
|
||||
typeof options === 'string' || options === undefined
|
||||
@@ -53,26 +52,32 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('closeWindow')
|
||||
closeWindow(data: undefined, sender: IpcClientEventSender) {
|
||||
this.app.browserManager.closeWindow(sender.identifier);
|
||||
@IpcMethod()
|
||||
closeWindow() {
|
||||
this.withSenderIdentifier((identifier) => {
|
||||
this.app.browserManager.closeWindow(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
@ipcClientEvent('minimizeWindow')
|
||||
minimizeWindow(data: undefined, sender: IpcClientEventSender) {
|
||||
this.app.browserManager.minimizeWindow(sender.identifier);
|
||||
@IpcMethod()
|
||||
minimizeWindow() {
|
||||
this.withSenderIdentifier((identifier) => {
|
||||
this.app.browserManager.minimizeWindow(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
@ipcClientEvent('maximizeWindow')
|
||||
maximizeWindow(data: undefined, sender: IpcClientEventSender) {
|
||||
this.app.browserManager.maximizeWindow(sender.identifier);
|
||||
@IpcMethod()
|
||||
maximizeWindow() {
|
||||
this.withSenderIdentifier((identifier) => {
|
||||
this.app.browserManager.maximizeWindow(identifier);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle route interception requests
|
||||
* Responsible for handling route interception requests from the renderer process
|
||||
*/
|
||||
@ipcClientEvent('interceptRoute')
|
||||
@IpcMethod()
|
||||
async interceptRoute(params: InterceptRouteParams) {
|
||||
const { path, source } = params;
|
||||
console.log(
|
||||
@@ -115,7 +120,7 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
/**
|
||||
* Create a new multi-instance window
|
||||
*/
|
||||
@ipcClientEvent('createMultiInstanceWindow')
|
||||
@IpcMethod()
|
||||
async createMultiInstanceWindow(params: {
|
||||
path: string;
|
||||
templateId: WindowTemplateIdentifiers;
|
||||
@@ -149,7 +154,7 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
/**
|
||||
* Get all windows by template
|
||||
*/
|
||||
@ipcClientEvent('getWindowsByTemplate')
|
||||
@IpcMethod()
|
||||
async getWindowsByTemplate(templateId: string) {
|
||||
try {
|
||||
const windowIds = this.app.browserManager.getWindowsByTemplate(templateId);
|
||||
@@ -169,7 +174,7 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
/**
|
||||
* Close all windows by template
|
||||
*/
|
||||
@ipcClientEvent('closeWindowsByTemplate')
|
||||
@IpcMethod()
|
||||
async closeWindowsByTemplate(templateId: string) {
|
||||
try {
|
||||
this.app.browserManager.closeWindowsByTemplate(templateId);
|
||||
@@ -191,4 +196,12 @@ export default class BrowserWindowsCtr extends ControllerModule {
|
||||
const browser = this.app.browserManager.retrieveByIdentifier(targetWindow);
|
||||
browser.show();
|
||||
}
|
||||
|
||||
private withSenderIdentifier(fn: (identifier: string) => void) {
|
||||
const context = getIpcContext();
|
||||
if (!context) return;
|
||||
const identifier = this.app.browserManager.getIdentifierByWebContents(context.sender);
|
||||
if (!identifier) return;
|
||||
fn(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
export default class DevtoolsCtr extends ControllerModule {
|
||||
@ipcClientEvent('openDevtools')
|
||||
static override readonly groupName = 'devtools';
|
||||
|
||||
@IpcMethod()
|
||||
async openDevtools() {
|
||||
const devtoolsBrowser = this.app.browserManager.retrieveByIdentifier('devtools');
|
||||
devtoolsBrowser.show();
|
||||
|
||||
@@ -30,19 +30,20 @@ import { FileResult, SearchOptions } from '@/types/fileSearch';
|
||||
import { makeSureDirExist } from '@/utils/file-system';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
// Create logger
|
||||
const logger = createLogger('controllers:LocalFileCtr');
|
||||
|
||||
export default class LocalFileCtr extends ControllerModule {
|
||||
static override readonly groupName = 'localSystem';
|
||||
private get searchService() {
|
||||
return this.app.getService(FileSearchService);
|
||||
}
|
||||
|
||||
// ==================== File Operation ====================
|
||||
|
||||
@ipcClientEvent('openLocalFile')
|
||||
@IpcMethod()
|
||||
async handleOpenLocalFile({ path: filePath }: OpenLocalFileParams): Promise<{
|
||||
error?: string;
|
||||
success: boolean;
|
||||
@@ -59,7 +60,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('openLocalFolder')
|
||||
@IpcMethod()
|
||||
async handleOpenLocalFolder({ path: targetPath, isDirectory }: OpenLocalFolderParams): Promise<{
|
||||
error?: string;
|
||||
success: boolean;
|
||||
@@ -77,7 +78,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('readLocalFiles')
|
||||
@IpcMethod()
|
||||
async readFiles({ paths }: LocalReadFilesParams): Promise<LocalReadFileResult[]> {
|
||||
logger.debug('Starting batch file reading:', { count: paths.length });
|
||||
|
||||
@@ -94,7 +95,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
return results;
|
||||
}
|
||||
|
||||
@ipcClientEvent('readLocalFile')
|
||||
@IpcMethod()
|
||||
async readFile({
|
||||
path: filePath,
|
||||
loc,
|
||||
@@ -192,7 +193,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('listLocalFiles')
|
||||
@IpcMethod()
|
||||
async listLocalFiles({ path: dirPath }: ListLocalFileParams): Promise<FileResult[]> {
|
||||
logger.debug('Listing directory contents:', { dirPath });
|
||||
|
||||
@@ -250,7 +251,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('moveLocalFiles')
|
||||
@IpcMethod()
|
||||
async handleMoveFiles({ items }: MoveLocalFilesParams): Promise<LocalMoveFilesResultItem[]> {
|
||||
logger.debug('Starting batch file move:', { itemsCount: items?.length });
|
||||
|
||||
@@ -355,7 +356,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
return results;
|
||||
}
|
||||
|
||||
@ipcClientEvent('renameLocalFile')
|
||||
@IpcMethod()
|
||||
async handleRenameFile({
|
||||
path: currentPath,
|
||||
newName,
|
||||
@@ -440,7 +441,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('writeLocalFile')
|
||||
@IpcMethod()
|
||||
async handleWriteFile({ path: filePath, content }: WriteLocalFileParams) {
|
||||
const logPrefix = `[Writing file ${filePath}]`;
|
||||
logger.debug(`${logPrefix} Starting to write file`, { contentLength: content?.length });
|
||||
@@ -485,7 +486,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
/**
|
||||
* Handle IPC event for local file search
|
||||
*/
|
||||
@ipcClientEvent('searchLocalFiles')
|
||||
@IpcMethod()
|
||||
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
|
||||
logger.debug('Received file search request:', {
|
||||
directory: params.directory,
|
||||
@@ -523,7 +524,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('grepContent')
|
||||
@IpcMethod()
|
||||
async handleGrepContent(params: GrepContentParams): Promise<GrepContentResult> {
|
||||
const {
|
||||
pattern,
|
||||
@@ -639,7 +640,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('globLocalFiles')
|
||||
@IpcMethod()
|
||||
async handleGlobFiles({
|
||||
path: searchPath = process.cwd(),
|
||||
pattern,
|
||||
@@ -680,7 +681,7 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
|
||||
// ==================== File Editing ====================
|
||||
|
||||
@ipcClientEvent('editLocalFile')
|
||||
@IpcMethod()
|
||||
async handleEditFile({
|
||||
file_path: filePath,
|
||||
new_string,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
export default class MenuController extends ControllerModule {
|
||||
static override readonly groupName = 'menu';
|
||||
/**
|
||||
* Refresh menu
|
||||
*/
|
||||
@ipcClientEvent('refreshAppMenu')
|
||||
@IpcMethod()
|
||||
refreshAppMenu() {
|
||||
// Note: May need to decide whether to allow renderer process to refresh all menus based on specific circumstances
|
||||
return this.app.menuManager.refreshMenus();
|
||||
@@ -13,7 +14,7 @@ export default class MenuController extends ControllerModule {
|
||||
/**
|
||||
* Show context menu
|
||||
*/
|
||||
@ipcClientEvent('showContextMenu')
|
||||
@IpcMethod()
|
||||
showContextMenu(params: { data?: any; type: string }) {
|
||||
return this.app.menuManager.showContextMenu(params.type, params.data);
|
||||
}
|
||||
@@ -21,7 +22,7 @@ export default class MenuController extends ControllerModule {
|
||||
/**
|
||||
* Set development menu visibility
|
||||
*/
|
||||
@ipcClientEvent('setDevMenuVisibility')
|
||||
@IpcMethod()
|
||||
setDevMenuVisibility(visible: boolean) {
|
||||
// Call MenuManager method to rebuild application menu
|
||||
return this.app.menuManager.rebuildAppMenu({ showDevItems: visible });
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ProxyDispatcherManager,
|
||||
ProxyTestResult,
|
||||
} from '../modules/networkProxy';
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
// Create logger
|
||||
const logger = createLogger('controllers:NetworkProxyCtr');
|
||||
@@ -21,10 +21,11 @@ const logger = createLogger('controllers:NetworkProxyCtr');
|
||||
* 处理桌面应用的网络代理相关功能
|
||||
*/
|
||||
export default class NetworkProxyCtr extends ControllerModule {
|
||||
static override readonly groupName = 'networkProxy';
|
||||
/**
|
||||
* 获取代理设置
|
||||
*/
|
||||
@ipcClientEvent('getProxySettings')
|
||||
@IpcMethod()
|
||||
async getDesktopSettings(): Promise<NetworkProxySettings> {
|
||||
try {
|
||||
const settings = this.app.storeManager.get(
|
||||
@@ -45,32 +46,30 @@ export default class NetworkProxyCtr extends ControllerModule {
|
||||
/**
|
||||
* 设置代理配置
|
||||
*/
|
||||
@ipcClientEvent('setProxySettings')
|
||||
async setProxySettings(config: NetworkProxySettings): Promise<void> {
|
||||
@IpcMethod()
|
||||
async setProxySettings(config: Partial<NetworkProxySettings>): Promise<void> {
|
||||
try {
|
||||
// 验证配置
|
||||
const validation = ProxyConfigValidator.validate(config);
|
||||
if (!validation.isValid) {
|
||||
const errorMessage = `Invalid proxy configuration: ${validation.errors.join(', ')}`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// 获取当前配置
|
||||
const currentConfig = this.app.storeManager.get(
|
||||
'networkProxy',
|
||||
defaultProxySettings,
|
||||
) as NetworkProxySettings;
|
||||
|
||||
// 检查是否有变化
|
||||
if (isEqual(currentConfig, config)) {
|
||||
// 合并配置并验证
|
||||
const newConfig = merge({}, currentConfig, config);
|
||||
|
||||
const validation = ProxyConfigValidator.validate(newConfig);
|
||||
if (!validation.isValid) {
|
||||
const errorMessage = `Invalid proxy configuration: ${validation.errors.join(', ')}`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (isEqual(currentConfig, newConfig)) {
|
||||
logger.debug('Proxy settings unchanged, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
// 合并配置
|
||||
const newConfig = merge({}, currentConfig, config);
|
||||
|
||||
// 应用代理设置
|
||||
await ProxyDispatcherManager.applyProxySettings(newConfig);
|
||||
|
||||
@@ -92,7 +91,7 @@ export default class NetworkProxyCtr extends ControllerModule {
|
||||
/**
|
||||
* 测试代理连接
|
||||
*/
|
||||
@ipcClientEvent('testProxyConnection')
|
||||
@IpcMethod()
|
||||
async testProxyConnection(url: string): Promise<{ message?: string; success: boolean }> {
|
||||
try {
|
||||
const result = await ProxyConnectionTester.testConnection(url);
|
||||
@@ -112,7 +111,7 @@ export default class NetworkProxyCtr extends ControllerModule {
|
||||
/**
|
||||
* 测试指定代理配置
|
||||
*/
|
||||
@ipcClientEvent('testProxyConfig')
|
||||
@IpcMethod()
|
||||
async testProxyConfig({
|
||||
config,
|
||||
testUrl,
|
||||
|
||||
@@ -7,11 +7,12 @@ import { macOS, windows } from 'electron-is';
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
const logger = createLogger('controllers:NotificationCtr');
|
||||
|
||||
export default class NotificationCtr extends ControllerModule {
|
||||
static override readonly groupName = 'notification';
|
||||
/**
|
||||
* Set up desktop notifications after the application is ready
|
||||
*/
|
||||
@@ -51,7 +52,7 @@ export default class NotificationCtr extends ControllerModule {
|
||||
/**
|
||||
* Show system desktop notification (only when window is hidden)
|
||||
*/
|
||||
@ipcClientEvent('showDesktopNotification')
|
||||
@IpcMethod()
|
||||
async showDesktopNotification(
|
||||
params: ShowDesktopNotificationParams,
|
||||
): Promise<DesktopNotificationResult> {
|
||||
@@ -126,7 +127,7 @@ export default class NotificationCtr extends ControllerModule {
|
||||
/**
|
||||
* Check if the main window is hidden
|
||||
*/
|
||||
@ipcClientEvent('isMainWindowHidden')
|
||||
@IpcMethod()
|
||||
isMainWindowHidden(): boolean {
|
||||
try {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { URL } from 'node:url';
|
||||
import { OFFICIAL_CLOUD_SERVER } from '@/const/env';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
/**
|
||||
* Non-retryable OIDC error codes
|
||||
@@ -39,6 +39,7 @@ const logger = createLogger('controllers:RemoteServerConfigCtr');
|
||||
* Used to manage custom remote LobeChat server configuration
|
||||
*/
|
||||
export default class RemoteServerConfigCtr extends ControllerModule {
|
||||
static override readonly groupName = 'remoteServer';
|
||||
/**
|
||||
* Key used to store encrypted tokens in electron-store.
|
||||
*/
|
||||
@@ -47,7 +48,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
|
||||
/**
|
||||
* Get remote server configuration
|
||||
*/
|
||||
@ipcClientEvent('getRemoteServerConfig')
|
||||
@IpcMethod()
|
||||
async getRemoteServerConfig() {
|
||||
logger.debug('Getting remote server configuration');
|
||||
const { storeManager } = this.app;
|
||||
@@ -64,7 +65,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
|
||||
/**
|
||||
* Set remote server configuration
|
||||
*/
|
||||
@ipcClientEvent('setRemoteServerConfig')
|
||||
@IpcMethod()
|
||||
async setRemoteServerConfig(config: Partial<DataSyncConfig>) {
|
||||
logger.info(
|
||||
`Setting remote server storageMode: active=${config.active}, storageMode=${config.storageMode}, url=${config.remoteServerUrl}`,
|
||||
@@ -81,7 +82,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
|
||||
/**
|
||||
* Clear remote server configuration
|
||||
*/
|
||||
@ipcClientEvent('clearRemoteServerConfig')
|
||||
@IpcMethod()
|
||||
async clearRemoteServerConfig() {
|
||||
logger.info('Clearing remote server configuration');
|
||||
const { storeManager } = this.app;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { defaultProxySettings } from '@/const/store';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
// Create logger
|
||||
const logger = createLogger('controllers:RemoteServerSyncCtr');
|
||||
@@ -25,6 +25,7 @@ const logger = createLogger('controllers:RemoteServerSyncCtr');
|
||||
* For handling data synchronization with remote servers via IPC.
|
||||
*/
|
||||
export default class RemoteServerSyncCtr extends ControllerModule {
|
||||
static override readonly groupName = 'remoteServerSync';
|
||||
/**
|
||||
* Cached instance of RemoteServerConfigCtr
|
||||
*/
|
||||
@@ -345,7 +346,7 @@ export default class RemoteServerSyncCtr extends ControllerModule {
|
||||
* Handles the 'proxy-trpc-request' IPC call from the renderer process.
|
||||
* This method should be invoked by the ipcMain.handle setup in your main process entry point.
|
||||
*/
|
||||
@ipcClientEvent('proxyTRPCRequest')
|
||||
@IpcMethod()
|
||||
public async proxyTRPCRequest(args: ProxyTRPCRequestParams): Promise<ProxyTRPCRequestResult> {
|
||||
logger.debug('Received proxyTRPCRequest IPC call:', {
|
||||
headers: args.headers,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
const logger = createLogger('controllers:ShellCommandCtr');
|
||||
|
||||
@@ -24,10 +24,11 @@ interface ShellProcess {
|
||||
}
|
||||
|
||||
export default class ShellCommandCtr extends ControllerModule {
|
||||
static override readonly groupName = 'shellCommand';
|
||||
// Shell process management
|
||||
private shellProcesses = new Map<string, ShellProcess>();
|
||||
|
||||
@ipcClientEvent('runCommand')
|
||||
@IpcMethod()
|
||||
async handleRunCommand({
|
||||
command,
|
||||
description,
|
||||
@@ -153,7 +154,7 @@ export default class ShellCommandCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ipcClientEvent('getCommandOutput')
|
||||
@IpcMethod()
|
||||
async handleGetCommandOutput({
|
||||
filter,
|
||||
shell_id,
|
||||
@@ -212,7 +213,7 @@ export default class ShellCommandCtr extends ControllerModule {
|
||||
};
|
||||
}
|
||||
|
||||
@ipcClientEvent('killCommand')
|
||||
@IpcMethod()
|
||||
async handleKillCommand({ shell_id }: KillCommandParams): Promise<KillCommandResult> {
|
||||
const logPrefix = `[killCommand: ${shell_id}]`;
|
||||
logger.debug(`${logPrefix} Attempting to kill shell`);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ShortcutUpdateResult } from '@/core/ui/ShortcutManager';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from '.';
|
||||
import { ControllerModule, IpcMethod } from '.';
|
||||
|
||||
export default class ShortcutController extends ControllerModule {
|
||||
static override readonly groupName = 'shortcut';
|
||||
/**
|
||||
* Get all shortcut configurations
|
||||
*/
|
||||
@ipcClientEvent('getShortcutsConfig')
|
||||
@IpcMethod()
|
||||
getShortcutsConfig() {
|
||||
return this.app.shortcutManager.getShortcutsConfig();
|
||||
}
|
||||
@@ -14,7 +15,7 @@ export default class ShortcutController extends ControllerModule {
|
||||
/**
|
||||
* Update a single shortcut configuration
|
||||
*/
|
||||
@ipcClientEvent('updateShortcutConfig')
|
||||
@IpcMethod()
|
||||
updateShortcutConfig({
|
||||
id,
|
||||
accelerator,
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { ElectronAppState, ThemeMode } from '@lobechat/electron-client-ipc';
|
||||
import { app, nativeTheme, shell, systemPreferences } from 'electron';
|
||||
import { macOS } from 'electron-is';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
import { DB_SCHEMA_HASH_FILENAME, LOCAL_DATABASE_DIR, userDataDir } from '@/const/dir';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent, ipcServerEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
const logger = createLogger('controllers:SystemCtr');
|
||||
|
||||
export default class SystemController extends ControllerModule {
|
||||
static override readonly groupName = 'system';
|
||||
private systemThemeListenerInitialized = false;
|
||||
|
||||
/**
|
||||
@@ -26,7 +24,7 @@ export default class SystemController extends ControllerModule {
|
||||
* Handles the 'getDesktopAppState' IPC request.
|
||||
* Gathers essential application and system information.
|
||||
*/
|
||||
@ipcClientEvent('getDesktopAppState')
|
||||
@IpcMethod()
|
||||
async getAppState(): Promise<ElectronAppState> {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
@@ -56,13 +54,13 @@ export default class SystemController extends ControllerModule {
|
||||
/**
|
||||
* 检查可用性
|
||||
*/
|
||||
@ipcClientEvent('checkSystemAccessibility')
|
||||
@IpcMethod()
|
||||
checkAccessibilityForMacOS() {
|
||||
if (!macOS()) return;
|
||||
return systemPreferences.isTrustedAccessibilityClient(true);
|
||||
}
|
||||
|
||||
@ipcClientEvent('openExternalLink')
|
||||
@IpcMethod()
|
||||
openExternalLink(url: string) {
|
||||
return shell.openExternal(url);
|
||||
}
|
||||
@@ -70,7 +68,7 @@ export default class SystemController extends ControllerModule {
|
||||
/**
|
||||
* 更新应用语言设置
|
||||
*/
|
||||
@ipcClientEvent('updateLocale')
|
||||
@IpcMethod()
|
||||
async updateLocale(locale: string) {
|
||||
// 保存语言设置
|
||||
this.app.storeManager.set('locale', locale);
|
||||
@@ -82,7 +80,7 @@ export default class SystemController extends ControllerModule {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ipcClientEvent('updateThemeMode')
|
||||
@IpcMethod()
|
||||
async updateThemeModeHandler(themeMode: ThemeMode) {
|
||||
this.app.storeManager.set('themeMode', themeMode);
|
||||
this.app.browserManager.broadcastToAllWindows('themeChanged', { themeMode });
|
||||
@@ -91,34 +89,6 @@ export default class SystemController extends ControllerModule {
|
||||
this.app.browserManager.handleAppThemeChange();
|
||||
}
|
||||
|
||||
@ipcServerEvent('getDatabasePath')
|
||||
async getDatabasePath() {
|
||||
return join(this.app.appStoragePath, LOCAL_DATABASE_DIR);
|
||||
}
|
||||
|
||||
@ipcServerEvent('getDatabaseSchemaHash')
|
||||
async getDatabaseSchemaHash() {
|
||||
try {
|
||||
return readFileSync(this.DB_SCHEMA_HASH_PATH, 'utf8');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ipcServerEvent('getUserDataPath')
|
||||
async getUserDataPath() {
|
||||
return userDataDir;
|
||||
}
|
||||
|
||||
@ipcServerEvent('setDatabaseSchemaHash')
|
||||
async setDatabaseSchemaHash(hash: string) {
|
||||
writeFileSync(this.DB_SCHEMA_HASH_PATH, hash, 'utf8');
|
||||
}
|
||||
|
||||
private get DB_SCHEMA_HASH_PATH() {
|
||||
return join(this.app.appStoragePath, DB_SCHEMA_HASH_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize system theme listener to monitor OS theme changes
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { DB_SCHEMA_HASH_FILENAME, LOCAL_DATABASE_DIR, userDataDir } from '@/const/dir';
|
||||
|
||||
import { ControllerModule, IpcServerMethod } from './index';
|
||||
|
||||
export default class SystemServerCtr extends ControllerModule {
|
||||
static override readonly groupName = 'system';
|
||||
|
||||
@IpcServerMethod()
|
||||
async getDatabasePath() {
|
||||
return join(this.app.appStoragePath, LOCAL_DATABASE_DIR);
|
||||
}
|
||||
|
||||
@IpcServerMethod()
|
||||
async getDatabaseSchemaHash() {
|
||||
try {
|
||||
return readFileSync(this.DB_SCHEMA_HASH_PATH, 'utf8');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@IpcServerMethod()
|
||||
async getUserDataPath() {
|
||||
return userDataDir;
|
||||
}
|
||||
|
||||
@IpcServerMethod()
|
||||
async setDatabaseSchemaHash(hash: string) {
|
||||
writeFileSync(this.DB_SCHEMA_HASH_PATH, hash, 'utf8');
|
||||
}
|
||||
|
||||
private get DB_SCHEMA_HASH_PATH() {
|
||||
return join(this.app.appStoragePath, DB_SCHEMA_HASH_FILENAME);
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
// Create logger
|
||||
const logger = createLogger('controllers:TrayMenuCtr');
|
||||
|
||||
export default class TrayMenuCtr extends ControllerModule {
|
||||
static override readonly groupName = 'tray';
|
||||
async toggleMainWindow() {
|
||||
logger.debug('Toggle main window visibility via shortcut');
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
@@ -23,7 +24,7 @@ export default class TrayMenuCtr extends ControllerModule {
|
||||
* @param options Balloon options
|
||||
* @returns Operation result
|
||||
*/
|
||||
@ipcClientEvent('showTrayNotification')
|
||||
@IpcMethod()
|
||||
async showNotification(options: ShowTrayNotificationParams) {
|
||||
logger.debug('Show tray balloon notification');
|
||||
|
||||
@@ -52,7 +53,7 @@ export default class TrayMenuCtr extends ControllerModule {
|
||||
* @param options Icon options
|
||||
* @returns Operation result
|
||||
*/
|
||||
@ipcClientEvent('updateTrayIcon')
|
||||
@IpcMethod()
|
||||
async updateTrayIcon(options: UpdateTrayIconParams) {
|
||||
logger.debug('Update tray icon');
|
||||
|
||||
@@ -84,7 +85,7 @@ export default class TrayMenuCtr extends ControllerModule {
|
||||
* @param options Tooltip text options
|
||||
* @returns Operation result
|
||||
*/
|
||||
@ipcClientEvent('updateTrayTooltip')
|
||||
@IpcMethod()
|
||||
async updateTrayTooltip(options: UpdateTrayTooltipParams) {
|
||||
logger.debug('Update tray tooltip text');
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
const logger = createLogger('controllers:UpdaterCtr');
|
||||
|
||||
export default class UpdaterCtr extends ControllerModule {
|
||||
static override readonly groupName = 'autoUpdate';
|
||||
/**
|
||||
* Check for updates
|
||||
*/
|
||||
@ipcClientEvent('checkUpdate')
|
||||
@IpcMethod()
|
||||
async checkForUpdates() {
|
||||
logger.info('Check for updates requested');
|
||||
await this.app.updaterManager.checkForUpdates();
|
||||
@@ -17,7 +18,7 @@ export default class UpdaterCtr extends ControllerModule {
|
||||
/**
|
||||
* Download update
|
||||
*/
|
||||
@ipcClientEvent('downloadUpdate')
|
||||
@IpcMethod()
|
||||
async downloadUpdate() {
|
||||
logger.info('Download update requested');
|
||||
await this.app.updaterManager.downloadUpdate();
|
||||
@@ -26,7 +27,7 @@ export default class UpdaterCtr extends ControllerModule {
|
||||
/**
|
||||
* Quit application and install update
|
||||
*/
|
||||
@ipcClientEvent('installNow')
|
||||
@IpcMethod()
|
||||
quitAndInstallUpdate() {
|
||||
logger.info('Quit and install update requested');
|
||||
this.app.updaterManager.installNow();
|
||||
@@ -35,7 +36,7 @@ export default class UpdaterCtr extends ControllerModule {
|
||||
/**
|
||||
* Install update on next startup
|
||||
*/
|
||||
@ipcClientEvent('installLater')
|
||||
@IpcMethod()
|
||||
installLater() {
|
||||
logger.info('Install later requested');
|
||||
this.app.updaterManager.installLater();
|
||||
|
||||
@@ -1,39 +1,17 @@
|
||||
import { UploadFileParams } from '@lobechat/electron-client-ipc';
|
||||
import { CreateFileParams } from '@lobechat/electron-server-ipc';
|
||||
|
||||
import FileService from '@/services/fileSrv';
|
||||
|
||||
import { ControllerModule, ipcClientEvent, ipcServerEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
export default class UploadFileCtr extends ControllerModule {
|
||||
static override readonly groupName = 'upload';
|
||||
private get fileService() {
|
||||
return this.app.getService(FileService);
|
||||
}
|
||||
|
||||
@ipcClientEvent('createFile')
|
||||
@IpcMethod()
|
||||
async uploadFile(params: UploadFileParams) {
|
||||
return this.fileService.uploadFile(params);
|
||||
}
|
||||
|
||||
// ======== server event
|
||||
|
||||
@ipcServerEvent('getStaticFilePath')
|
||||
async getFileUrlById(id: string) {
|
||||
return this.fileService.getFilePath(id);
|
||||
}
|
||||
|
||||
@ipcServerEvent('getFileHTTPURL')
|
||||
async getFileHTTPURL(path: string) {
|
||||
return this.fileService.getFileHTTPURL(path);
|
||||
}
|
||||
|
||||
@ipcServerEvent('deleteFiles')
|
||||
async deleteFiles(paths: string[]) {
|
||||
return this.fileService.deleteFiles(paths);
|
||||
}
|
||||
|
||||
@ipcServerEvent('createFile')
|
||||
async createFile(params: CreateFileParams) {
|
||||
return this.fileService.uploadFile(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CreateFileParams } from '@lobechat/electron-server-ipc';
|
||||
|
||||
import FileService from '@/services/fileSrv';
|
||||
|
||||
import { ControllerModule, IpcServerMethod } from './index';
|
||||
|
||||
export default class UploadFileServerCtr extends ControllerModule {
|
||||
static override readonly groupName = 'upload';
|
||||
|
||||
private get fileService() {
|
||||
return this.app.getService(FileService);
|
||||
}
|
||||
|
||||
@IpcServerMethod()
|
||||
async getFileUrlById(id: string) {
|
||||
return this.fileService.getFilePath(id);
|
||||
}
|
||||
|
||||
@IpcServerMethod()
|
||||
async getFileHTTPURL(path: string) {
|
||||
return this.fileService.getFileHTTPURL(path);
|
||||
}
|
||||
|
||||
@IpcServerMethod()
|
||||
async deleteFiles(paths: string[]) {
|
||||
return this.fileService.deleteFiles(paths);
|
||||
}
|
||||
|
||||
@IpcServerMethod()
|
||||
async createFile(params: CreateFileParams) {
|
||||
return this.fileService.uploadFile(params);
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,18 @@ vi.mock('@/utils/logger', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock electron
|
||||
vi.mock('electron', () => ({
|
||||
BrowserWindow: {
|
||||
getAllWindows: vi.fn(() => []),
|
||||
},
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
shell: {
|
||||
openExternal: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
@@ -99,6 +106,7 @@ describe('AuthCtr', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcMainHandleMock.mockClear();
|
||||
randomBytesCounter = 0; // Reset counter for each test
|
||||
|
||||
// Reset shell.openExternal to default successful behavior
|
||||
@@ -123,7 +131,7 @@ describe('AuthCtr', () => {
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up authCtr intervals (using real timers, not fake timers)
|
||||
authCtr.cleanup();
|
||||
authCtr?.cleanup?.();
|
||||
// Clean up any fake timers if used
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
@@ -3,10 +3,21 @@ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
|
||||
import type { App } from '@/core/App';
|
||||
import type { IpcClientEventSender } from '@/types/ipcClientEvent';
|
||||
import type { IpcContext } from '@/utils/ipc';
|
||||
import { runWithIpcContext } from '@/utils/ipc';
|
||||
|
||||
import BrowserWindowsCtr from '../BrowserWindowsCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
// 模拟 App 及其依赖项
|
||||
const mockToggleVisible = vi.fn();
|
||||
const mockLoadUrl = vi.fn();
|
||||
@@ -16,6 +27,9 @@ const mockCloseWindow = vi.fn();
|
||||
const mockMinimizeWindow = vi.fn();
|
||||
const mockMaximizeWindow = vi.fn();
|
||||
const mockRetrieveByIdentifier = vi.fn();
|
||||
const testSenderIdentifierString: string = 'test-window-event-id';
|
||||
|
||||
const mockGetIdentifierByWebContents = vi.fn(() => testSenderIdentifierString);
|
||||
const mockGetMainWindow = vi.fn(() => ({
|
||||
toggleVisible: mockToggleVisible,
|
||||
loadUrl: mockLoadUrl,
|
||||
@@ -32,6 +46,7 @@ const { findMatchingRoute } = await import('~common/routes');
|
||||
|
||||
const mockApp = {
|
||||
browserManager: {
|
||||
getIdentifierByWebContents: mockGetIdentifierByWebContents,
|
||||
getMainWindow: mockGetMainWindow,
|
||||
redirectToPage: mockRedirectToPage,
|
||||
closeWindow: mockCloseWindow,
|
||||
@@ -53,6 +68,7 @@ describe('BrowserWindowsCtr', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcMainHandleMock.mockClear();
|
||||
browserWindowsCtr = new BrowserWindowsCtr(mockApp);
|
||||
});
|
||||
|
||||
@@ -82,28 +98,32 @@ describe('BrowserWindowsCtr', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const testSenderIdentifierString: string = 'test-window-event-id';
|
||||
const sender: IpcClientEventSender = {
|
||||
identifier: testSenderIdentifierString,
|
||||
};
|
||||
|
||||
describe('closeWindow', () => {
|
||||
it('should close the window with the given sender identifier', () => {
|
||||
browserWindowsCtr.closeWindow(undefined, sender);
|
||||
const sender = {} as any;
|
||||
const context = { sender, event: { sender } as any } as IpcContext;
|
||||
runWithIpcContext(context, () => browserWindowsCtr.closeWindow());
|
||||
expect(mockGetIdentifierByWebContents).toHaveBeenCalledWith(context.sender);
|
||||
expect(mockCloseWindow).toHaveBeenCalledWith(testSenderIdentifierString);
|
||||
});
|
||||
});
|
||||
|
||||
describe('minimizeWindow', () => {
|
||||
it('should minimize the window with the given sender identifier', () => {
|
||||
browserWindowsCtr.minimizeWindow(undefined, sender);
|
||||
const sender = {} as any;
|
||||
const context = { sender, event: { sender } as any } as IpcContext;
|
||||
runWithIpcContext(context, () => browserWindowsCtr.minimizeWindow());
|
||||
expect(mockGetIdentifierByWebContents).toHaveBeenCalledWith(context.sender);
|
||||
expect(mockMinimizeWindow).toHaveBeenCalledWith(testSenderIdentifierString);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maximizeWindow', () => {
|
||||
it('should maximize the window with the given sender identifier', () => {
|
||||
browserWindowsCtr.maximizeWindow(undefined, sender);
|
||||
const sender = {} as any;
|
||||
const context = { sender, event: { sender } as any } as IpcContext;
|
||||
runWithIpcContext(context, () => browserWindowsCtr.maximizeWindow());
|
||||
expect(mockGetIdentifierByWebContents).toHaveBeenCalledWith(context.sender);
|
||||
expect(mockMaximizeWindow).toHaveBeenCalledWith(testSenderIdentifierString);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,16 @@ import type { App } from '@/core/App';
|
||||
|
||||
import DevtoolsCtr from '../DevtoolsCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
// 模拟 App 及其依赖项
|
||||
const mockShow = vi.fn();
|
||||
const mockRetrieveByIdentifier = vi.fn(() => ({
|
||||
@@ -24,10 +34,9 @@ describe('DevtoolsCtr', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks(); // 只清除 vi.fn() 创建的模拟函数的记录,不影响 IoCContainer 状态
|
||||
ipcMainHandleMock.mockClear();
|
||||
|
||||
// 实例化 DevtoolsCtr。
|
||||
// 它将继承自真实的 ControllerModule。
|
||||
// 其 @ipcClientEvent 装饰器会执行并与真实的 IoCContainer 交互。
|
||||
// 实例化 DevtoolsCtr。其 @IpcMethod 装饰器会执行并与真实的 IoCContainer 交互。
|
||||
devtoolsCtr = new DevtoolsCtr(mockApp);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import type { App } from '@/core/App';
|
||||
|
||||
import LocalFileCtr from '../LocalFileCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -22,6 +26,9 @@ vi.mock('@lobechat/file-loaders', () => ({
|
||||
|
||||
// Mock electron
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
shell: {
|
||||
openPath: vi.fn(),
|
||||
},
|
||||
|
||||
@@ -4,6 +4,16 @@ import type { App } from '@/core/App';
|
||||
|
||||
import MenuController from '../MenuCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
// 模拟 App 及其依赖项
|
||||
const mockRefreshMenus = vi.fn();
|
||||
const mockShowContextMenu = vi.fn();
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { App } from '@/core/App';
|
||||
|
||||
import NetworkProxyCtr from '../NetworkProxyCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// 模拟 logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -54,6 +58,7 @@ describe('NetworkProxyCtr', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
ipcMainHandleMock.mockClear();
|
||||
|
||||
// 动态导入 undici Mock
|
||||
mockUndici = await import('undici');
|
||||
@@ -418,3 +423,8 @@ describe('NetworkProxyCtr', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { App } from '@/core/App';
|
||||
|
||||
import NotificationCtr from '../NotificationCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -25,6 +29,9 @@ vi.mock('electron', () => {
|
||||
MockNotification.isSupported = vi.fn(() => true);
|
||||
|
||||
return {
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
Notification: MockNotification,
|
||||
app: {
|
||||
setAppUserModelId: vi.fn(),
|
||||
@@ -65,6 +72,7 @@ describe('NotificationCtr', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcMainHandleMock.mockClear();
|
||||
vi.useFakeTimers();
|
||||
controller = new NotificationCtr(mockApp);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { App } from '@/core/App';
|
||||
|
||||
import RemoteServerConfigCtr from '../RemoteServerConfigCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -17,6 +21,9 @@ vi.mock('@/utils/logger', () => ({
|
||||
|
||||
// Mock electron
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
safeStorage: {
|
||||
decryptString: vi.fn((buffer: Buffer) => buffer.toString()),
|
||||
encryptString: vi.fn((str: string) => Buffer.from(str)),
|
||||
@@ -45,6 +52,7 @@ describe('RemoteServerConfigCtr', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcMainHandleMock.mockClear();
|
||||
mockStoreManager.get.mockReturnValue({
|
||||
active: false,
|
||||
storageMode: 'local',
|
||||
|
||||
@@ -22,6 +22,7 @@ vi.mock('electron', () => ({
|
||||
getPath: vi.fn(() => '/mock/user/data'),
|
||||
},
|
||||
ipcMain: {
|
||||
handle: vi.fn(),
|
||||
on: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -4,6 +4,16 @@ import type { App } from '@/core/App';
|
||||
|
||||
import ShellCommandCtr from '../ShellCommandCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
|
||||
@@ -4,6 +4,16 @@ import type { App } from '@/core/App';
|
||||
|
||||
import ShortcutController from '../ShortcutCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
// 模拟 App 及其依赖项
|
||||
const mockGetShortcutsConfig = vi.fn().mockReturnValue({
|
||||
toggleMainWindow: 'CommandOrControl+Shift+L',
|
||||
@@ -26,6 +36,7 @@ describe('ShortcutController', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcMainHandleMock.mockClear();
|
||||
shortcutController = new ShortcutController(mockApp);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,9 +2,38 @@ import { ThemeMode } from '@lobechat/electron-client-ipc';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { App } from '@/core/App';
|
||||
import type { IpcContext } from '@/utils/ipc';
|
||||
import { IpcHandler } from '@/utils/ipc/base';
|
||||
|
||||
import SystemController from '../SystemCtr';
|
||||
|
||||
const { ipcHandlers, ipcMainHandleMock } = vi.hoisted(() => {
|
||||
const handlers = new Map<string, (event: any, ...args: any[]) => any>();
|
||||
const handle = vi.fn((channel: string, handler: any) => {
|
||||
handlers.set(channel, handler);
|
||||
});
|
||||
return { ipcHandlers: handlers, ipcMainHandleMock: handle };
|
||||
});
|
||||
|
||||
const invokeIpc = async <T = any>(
|
||||
channel: string,
|
||||
payload?: any,
|
||||
context?: Partial<IpcContext>,
|
||||
): Promise<T> => {
|
||||
const handler = ipcHandlers.get(channel);
|
||||
if (!handler) throw new Error(`IPC handler for ${channel} not found`);
|
||||
|
||||
const fakeEvent = {
|
||||
sender: context?.sender ?? ({ id: 'test' } as any),
|
||||
};
|
||||
|
||||
if (payload === undefined) {
|
||||
return handler(fakeEvent);
|
||||
}
|
||||
|
||||
return handler(fakeEvent, payload);
|
||||
};
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -21,6 +50,9 @@ vi.mock('electron', () => ({
|
||||
getLocale: vi.fn(() => 'en-US'),
|
||||
getPath: vi.fn((name: string) => `/mock/path/${name}`),
|
||||
},
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
nativeTheme: {
|
||||
on: vi.fn(),
|
||||
shouldUseDarkColors: false,
|
||||
@@ -38,19 +70,6 @@ vi.mock('electron-is', () => ({
|
||||
macOS: vi.fn(() => true),
|
||||
}));
|
||||
|
||||
// Mock node:fs
|
||||
vi.mock('node:fs', () => ({
|
||||
readFileSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock @/const/dir
|
||||
vi.mock('@/const/dir', () => ({
|
||||
DB_SCHEMA_HASH_FILENAME: 'db-schema-hash.txt',
|
||||
LOCAL_DATABASE_DIR: 'database',
|
||||
userDataDir: '/mock/user/data',
|
||||
}));
|
||||
|
||||
// Mock browserManager
|
||||
const mockBrowserManager = {
|
||||
broadcastToAllWindows: vi.fn(),
|
||||
@@ -80,12 +99,15 @@ describe('SystemController', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcHandlers.clear();
|
||||
ipcMainHandleMock.mockClear();
|
||||
(IpcHandler.getInstance() as any).registeredChannels?.clear();
|
||||
controller = new SystemController(mockApp);
|
||||
});
|
||||
|
||||
describe('getAppState', () => {
|
||||
it('should return app state with system info', async () => {
|
||||
const result = await controller.getAppState();
|
||||
const result = await invokeIpc('system.getAppState');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
arch: expect.any(String),
|
||||
@@ -108,7 +130,7 @@ describe('SystemController', () => {
|
||||
const { nativeTheme } = await import('electron');
|
||||
Object.defineProperty(nativeTheme, 'shouldUseDarkColors', { value: true });
|
||||
|
||||
const result = await controller.getAppState();
|
||||
const result = await invokeIpc('system.getAppState');
|
||||
|
||||
expect(result.systemAppearance).toBe('dark');
|
||||
|
||||
@@ -121,7 +143,7 @@ describe('SystemController', () => {
|
||||
it('should check accessibility on macOS', async () => {
|
||||
const { systemPreferences } = await import('electron');
|
||||
|
||||
controller.checkAccessibilityForMacOS();
|
||||
await invokeIpc('system.checkAccessibilityForMacOS');
|
||||
|
||||
expect(systemPreferences.isTrustedAccessibilityClient).toHaveBeenCalledWith(true);
|
||||
});
|
||||
@@ -130,7 +152,7 @@ describe('SystemController', () => {
|
||||
const { macOS } = await import('electron-is');
|
||||
vi.mocked(macOS).mockReturnValue(false);
|
||||
|
||||
const result = controller.checkAccessibilityForMacOS();
|
||||
const result = await invokeIpc('system.checkAccessibilityForMacOS');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
@@ -143,7 +165,7 @@ describe('SystemController', () => {
|
||||
it('should open external link', async () => {
|
||||
const { shell } = await import('electron');
|
||||
|
||||
await controller.openExternalLink('https://example.com');
|
||||
await invokeIpc('system.openExternalLink', 'https://example.com');
|
||||
|
||||
expect(shell.openExternal).toHaveBeenCalledWith('https://example.com');
|
||||
});
|
||||
@@ -151,7 +173,7 @@ describe('SystemController', () => {
|
||||
|
||||
describe('updateLocale', () => {
|
||||
it('should update locale and broadcast change', async () => {
|
||||
const result = await controller.updateLocale('zh-CN');
|
||||
const result = await invokeIpc('system.updateLocale', 'zh-CN');
|
||||
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith('locale', 'zh-CN');
|
||||
expect(mockI18n.changeLanguage).toHaveBeenCalledWith('zh-CN');
|
||||
@@ -162,7 +184,7 @@ describe('SystemController', () => {
|
||||
});
|
||||
|
||||
it('should use system locale when set to auto', async () => {
|
||||
await controller.updateLocale('auto');
|
||||
await invokeIpc('system.updateLocale', 'auto');
|
||||
|
||||
expect(mockI18n.changeLanguage).toHaveBeenCalledWith('en-US');
|
||||
});
|
||||
@@ -172,7 +194,7 @@ describe('SystemController', () => {
|
||||
it('should update theme mode and broadcast change', async () => {
|
||||
const themeMode: ThemeMode = 'dark';
|
||||
|
||||
await controller.updateThemeModeHandler(themeMode);
|
||||
await invokeIpc('system.updateThemeModeHandler', themeMode);
|
||||
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith('themeMode', 'dark');
|
||||
expect(mockBrowserManager.broadcastToAllWindows).toHaveBeenCalledWith('themeChanged', {
|
||||
@@ -182,58 +204,6 @@ describe('SystemController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDatabasePath', () => {
|
||||
it('should return database path', async () => {
|
||||
const result = await controller.getDatabasePath();
|
||||
|
||||
expect(result).toBe('/mock/storage/database');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDatabaseSchemaHash', () => {
|
||||
it('should return schema hash when file exists', async () => {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
vi.mocked(readFileSync).mockReturnValue('abc123');
|
||||
|
||||
const result = await controller.getDatabaseSchemaHash();
|
||||
|
||||
expect(result).toBe('abc123');
|
||||
});
|
||||
|
||||
it('should return undefined when file does not exist', async () => {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
vi.mocked(readFileSync).mockImplementation(() => {
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
const result = await controller.getDatabaseSchemaHash();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserDataPath', () => {
|
||||
it('should return user data path', async () => {
|
||||
const result = await controller.getUserDataPath();
|
||||
|
||||
expect(result).toBe('/mock/user/data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDatabaseSchemaHash', () => {
|
||||
it('should write schema hash to file', async () => {
|
||||
const { writeFileSync } = await import('node:fs');
|
||||
|
||||
await controller.setDatabaseSchemaHash('newhash123');
|
||||
|
||||
expect(writeFileSync).toHaveBeenCalledWith(
|
||||
'/mock/storage/db-schema-hash.txt',
|
||||
'newhash123',
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterAppReady', () => {
|
||||
it('should initialize system theme listener', async () => {
|
||||
const { nativeTheme } = await import('electron');
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { App } from '@/core/App';
|
||||
|
||||
import SystemServerCtr from '../SystemServerCtr';
|
||||
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
readFileSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/const/dir', () => ({
|
||||
DB_SCHEMA_HASH_FILENAME: 'db-schema-hash.txt',
|
||||
LOCAL_DATABASE_DIR: 'database',
|
||||
userDataDir: '/mock/user/data',
|
||||
}));
|
||||
|
||||
const mockApp = {
|
||||
appStoragePath: '/mock/storage',
|
||||
} as unknown as App;
|
||||
|
||||
describe('SystemServerCtr', () => {
|
||||
let controller: SystemServerCtr;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
controller = new SystemServerCtr(mockApp);
|
||||
});
|
||||
|
||||
it('returns database path', async () => {
|
||||
await expect(controller.getDatabasePath()).resolves.toBe('/mock/storage/database');
|
||||
});
|
||||
|
||||
it('reads schema hash when file exists', async () => {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
vi.mocked(readFileSync).mockReturnValue('hash123');
|
||||
|
||||
await expect(controller.getDatabaseSchemaHash()).resolves.toBe('hash123');
|
||||
expect(readFileSync).toHaveBeenCalledWith('/mock/storage/db-schema-hash.txt', 'utf8');
|
||||
});
|
||||
|
||||
it('returns undefined when schema hash file missing', async () => {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
vi.mocked(readFileSync).mockImplementation(() => {
|
||||
throw new Error('missing');
|
||||
});
|
||||
|
||||
await expect(controller.getDatabaseSchemaHash()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns user data path', async () => {
|
||||
await expect(controller.getUserDataPath()).resolves.toBe('/mock/user/data');
|
||||
});
|
||||
|
||||
it('writes schema hash to disk', async () => {
|
||||
const { writeFileSync } = await import('node:fs');
|
||||
|
||||
await controller.setDatabaseSchemaHash('newhash');
|
||||
|
||||
expect(writeFileSync).toHaveBeenCalledWith('/mock/storage/db-schema-hash.txt', 'newhash', 'utf8');
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,24 @@
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
import {
|
||||
ShowTrayNotificationParams,
|
||||
UpdateTrayIconParams,
|
||||
UpdateTrayTooltipParams
|
||||
UpdateTrayTooltipParams,
|
||||
} from '@lobechat/electron-client-ipc';
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { App } from '@/core/App';
|
||||
|
||||
import TrayMenuCtr from '../TrayMenuCtr';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
// 模拟 logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -15,8 +27,6 @@ vi.mock('@/utils/logger', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
import TrayMenuCtr from '../TrayMenuCtr';
|
||||
|
||||
// 保存原始平台,确保测试结束后能恢复
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
@@ -45,6 +55,7 @@ describe('TrayMenuCtr', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcMainHandleMock.mockClear();
|
||||
// 为每个测试重置 mockedTray
|
||||
mockGetMainTray.mockReset();
|
||||
trayMenuCtr = new TrayMenuCtr(mockApp);
|
||||
@@ -69,7 +80,7 @@ describe('TrayMenuCtr', () => {
|
||||
it('should display balloon notification on Windows platform', async () => {
|
||||
// 模拟 Windows 平台
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
|
||||
|
||||
const mockedTray = {
|
||||
displayBalloon: mockDisplayBalloon,
|
||||
};
|
||||
@@ -125,9 +136,9 @@ describe('TrayMenuCtr', () => {
|
||||
|
||||
expect(mockGetMainTray).toHaveBeenCalled();
|
||||
expect(mockDisplayBalloon).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
expect(result).toEqual({
|
||||
error: 'Tray notifications are only supported on Windows platform',
|
||||
success: false
|
||||
success: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -136,7 +147,7 @@ describe('TrayMenuCtr', () => {
|
||||
it('should update tray icon on Windows platform', async () => {
|
||||
// 模拟 Windows 平台
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
|
||||
|
||||
const mockedTray = {
|
||||
updateIcon: mockUpdateIcon,
|
||||
};
|
||||
@@ -156,7 +167,7 @@ describe('TrayMenuCtr', () => {
|
||||
it('should handle errors when updating icon', async () => {
|
||||
// 模拟 Windows 平台
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
|
||||
|
||||
const error = new Error('Failed to update icon');
|
||||
const mockedTray = {
|
||||
updateIcon: vi.fn().mockImplementation(() => {
|
||||
@@ -198,7 +209,7 @@ describe('TrayMenuCtr', () => {
|
||||
it('should update tray tooltip on Windows platform', async () => {
|
||||
// 模拟 Windows 平台
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
|
||||
|
||||
const mockedTray = {
|
||||
updateTooltip: mockUpdateTooltip,
|
||||
};
|
||||
@@ -234,7 +245,7 @@ describe('TrayMenuCtr', () => {
|
||||
it('should return error when tooltip is not provided', async () => {
|
||||
// 模拟 Windows 平台
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
|
||||
|
||||
const mockedTray = {
|
||||
updateTooltip: mockUpdateTooltip,
|
||||
};
|
||||
@@ -253,4 +264,4 @@ describe('TrayMenuCtr', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { App } from '@/core/App';
|
||||
|
||||
import UpdaterCtr from '../UpdaterCtr';
|
||||
|
||||
// 模拟 logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -9,7 +11,15 @@ vi.mock('@/utils/logger', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
import UpdaterCtr from '../UpdaterCtr';
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
// 模拟 App 及其依赖项
|
||||
const mockCheckForUpdates = vi.fn();
|
||||
@@ -31,6 +41,7 @@ describe('UpdaterCtr', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcMainHandleMock.mockClear();
|
||||
updaterCtr = new UpdaterCtr(mockApp);
|
||||
});
|
||||
|
||||
@@ -79,4 +90,4 @@ describe('UpdaterCtr', () => {
|
||||
await expect(updaterCtr.downloadUpdate()).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { App } from '@/core/App';
|
||||
import { IpcHandler } from '@/utils/ipc/base';
|
||||
|
||||
import UploadFileCtr from '../UploadFileCtr';
|
||||
|
||||
const { ipcHandlers, ipcMainHandleMock } = vi.hoisted(() => {
|
||||
const handlers = new Map<string, (event: any, ...args: any[]) => any>();
|
||||
const handle = vi.fn((channel: string, handler: any) => {
|
||||
handlers.set(channel, handler);
|
||||
});
|
||||
return { ipcHandlers: handlers, ipcMainHandleMock: handle };
|
||||
});
|
||||
|
||||
const invokeIpc = async <T = any>(channel: string, payload?: any): Promise<T> => {
|
||||
const handler = ipcHandlers.get(channel);
|
||||
if (!handler) throw new Error(`IPC handler for ${channel} not found`);
|
||||
|
||||
const fakeEvent = { sender: { id: 'test' } as any };
|
||||
if (payload === undefined) return handler(fakeEvent);
|
||||
return handler(fakeEvent, payload);
|
||||
};
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock FileService module to prevent electron dependency issues
|
||||
vi.mock('@/services/fileSrv', () => ({
|
||||
default: class MockFileService {},
|
||||
@@ -12,9 +36,6 @@ vi.mock('@/services/fileSrv', () => ({
|
||||
// Mock FileService instance methods
|
||||
const mockFileService = {
|
||||
uploadFile: vi.fn(),
|
||||
getFilePath: vi.fn(),
|
||||
getFileHTTPURL: vi.fn(),
|
||||
deleteFiles: vi.fn(),
|
||||
};
|
||||
|
||||
const mockApp = {
|
||||
@@ -26,6 +47,9 @@ describe('UploadFileCtr', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcHandlers.clear();
|
||||
ipcMainHandleMock.mockClear();
|
||||
(IpcHandler.getInstance() as any).registeredChannels?.clear();
|
||||
controller = new UploadFileCtr(mockApp);
|
||||
});
|
||||
|
||||
@@ -41,7 +65,7 @@ describe('UploadFileCtr', () => {
|
||||
const expectedResult = { id: 'file-id-123', url: '/files/file-id-123' };
|
||||
mockFileService.uploadFile.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.uploadFile(params);
|
||||
const result = await invokeIpc('upload.uploadFile', params);
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(mockFileService.uploadFile).toHaveBeenCalledWith(params);
|
||||
@@ -58,110 +82,7 @@ describe('UploadFileCtr', () => {
|
||||
const error = new Error('Upload failed');
|
||||
mockFileService.uploadFile.mockRejectedValue(error);
|
||||
|
||||
await expect(controller.uploadFile(params)).rejects.toThrow('Upload failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileUrlById', () => {
|
||||
it('should get file path by id successfully', async () => {
|
||||
const fileId = 'file-id-123';
|
||||
const expectedPath = '/files/abc123.txt';
|
||||
mockFileService.getFilePath.mockResolvedValue(expectedPath);
|
||||
|
||||
const result = await controller.getFileUrlById(fileId);
|
||||
|
||||
expect(result).toBe(expectedPath);
|
||||
expect(mockFileService.getFilePath).toHaveBeenCalledWith(fileId);
|
||||
});
|
||||
|
||||
it('should handle get file path error', async () => {
|
||||
const fileId = 'non-existent-id';
|
||||
const error = new Error('File not found');
|
||||
mockFileService.getFilePath.mockRejectedValue(error);
|
||||
|
||||
await expect(controller.getFileUrlById(fileId)).rejects.toThrow('File not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileHTTPURL', () => {
|
||||
it('should get file HTTP URL successfully', async () => {
|
||||
const filePath = '/files/abc123.txt';
|
||||
const expectedUrl = 'http://localhost:3000/files/abc123.txt';
|
||||
mockFileService.getFileHTTPURL.mockResolvedValue(expectedUrl);
|
||||
|
||||
const result = await controller.getFileHTTPURL(filePath);
|
||||
|
||||
expect(result).toBe(expectedUrl);
|
||||
expect(mockFileService.getFileHTTPURL).toHaveBeenCalledWith(filePath);
|
||||
});
|
||||
|
||||
it('should handle get HTTP URL error', async () => {
|
||||
const filePath = '/files/abc123.txt';
|
||||
const error = new Error('Failed to generate URL');
|
||||
mockFileService.getFileHTTPURL.mockRejectedValue(error);
|
||||
|
||||
await expect(controller.getFileHTTPURL(filePath)).rejects.toThrow('Failed to generate URL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFiles', () => {
|
||||
it('should delete files successfully', async () => {
|
||||
const paths = ['/files/file1.txt', '/files/file2.txt'];
|
||||
mockFileService.deleteFiles.mockResolvedValue(undefined);
|
||||
|
||||
await controller.deleteFiles(paths);
|
||||
|
||||
expect(mockFileService.deleteFiles).toHaveBeenCalledWith(paths);
|
||||
});
|
||||
|
||||
it('should handle delete files error', async () => {
|
||||
const paths = ['/files/file1.txt'];
|
||||
const error = new Error('Delete failed');
|
||||
mockFileService.deleteFiles.mockRejectedValue(error);
|
||||
|
||||
await expect(controller.deleteFiles(paths)).rejects.toThrow('Delete failed');
|
||||
});
|
||||
|
||||
it('should handle empty paths array', async () => {
|
||||
const paths: string[] = [];
|
||||
mockFileService.deleteFiles.mockResolvedValue(undefined);
|
||||
|
||||
await controller.deleteFiles(paths);
|
||||
|
||||
expect(mockFileService.deleteFiles).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFile', () => {
|
||||
it('should create file successfully', async () => {
|
||||
const params = {
|
||||
hash: 'xyz789',
|
||||
path: '/test/newfile.txt',
|
||||
content: 'bmV3IGZpbGUgY29udGVudA==',
|
||||
filename: 'newfile.txt',
|
||||
type: 'text/plain',
|
||||
};
|
||||
const expectedResult = { id: 'new-file-id', url: '/files/new-file-id' };
|
||||
mockFileService.uploadFile.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.createFile(params);
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(mockFileService.uploadFile).toHaveBeenCalledWith(params);
|
||||
});
|
||||
|
||||
it('should handle create file error', async () => {
|
||||
const params = {
|
||||
hash: 'xyz789',
|
||||
path: '/test/newfile.txt',
|
||||
content: 'bmV3IGZpbGUgY29udGVudA==',
|
||||
filename: 'newfile.txt',
|
||||
type: 'text/plain',
|
||||
};
|
||||
const error = new Error('Create failed');
|
||||
mockFileService.uploadFile.mockRejectedValue(error);
|
||||
|
||||
await expect(controller.createFile(params)).rejects.toThrow('Create failed');
|
||||
await expect(invokeIpc('upload.uploadFile', params)).rejects.toThrow('Upload failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { App } from '@/core/App';
|
||||
|
||||
import UploadFileServerCtr from '../UploadFileServerCtr';
|
||||
|
||||
vi.mock('@/services/fileSrv', () => ({
|
||||
default: class MockFileService {},
|
||||
}));
|
||||
|
||||
const mockFileService = {
|
||||
getFileHTTPURL: vi.fn(),
|
||||
getFilePath: vi.fn(),
|
||||
deleteFiles: vi.fn(),
|
||||
uploadFile: vi.fn(),
|
||||
};
|
||||
|
||||
const mockApp = {
|
||||
getService: vi.fn(() => mockFileService),
|
||||
} as unknown as App;
|
||||
|
||||
describe('UploadFileServerCtr', () => {
|
||||
let controller: UploadFileServerCtr;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
controller = new UploadFileServerCtr(mockApp);
|
||||
});
|
||||
|
||||
it('gets file path by id', async () => {
|
||||
mockFileService.getFilePath.mockResolvedValue('path');
|
||||
await expect(controller.getFileUrlById('id')).resolves.toBe('path');
|
||||
expect(mockFileService.getFilePath).toHaveBeenCalledWith('id');
|
||||
});
|
||||
|
||||
it('gets HTTP URL', async () => {
|
||||
mockFileService.getFileHTTPURL.mockResolvedValue('url');
|
||||
await expect(controller.getFileHTTPURL('/path')).resolves.toBe('url');
|
||||
expect(mockFileService.getFileHTTPURL).toHaveBeenCalledWith('/path');
|
||||
});
|
||||
|
||||
it('deletes files', async () => {
|
||||
mockFileService.deleteFiles.mockResolvedValue(undefined);
|
||||
await controller.deleteFiles(['a']);
|
||||
expect(mockFileService.deleteFiles).toHaveBeenCalledWith(['a']);
|
||||
});
|
||||
|
||||
it('creates files via upload service', async () => {
|
||||
const params = { filename: 'file' } as any;
|
||||
mockFileService.uploadFile.mockResolvedValue({ success: true });
|
||||
|
||||
await expect(controller.createFile(params)).resolves.toEqual({ success: true });
|
||||
expect(mockFileService.uploadFile).toHaveBeenCalledWith(params);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
import { ControllerModule, IpcMethod } from './index';
|
||||
|
||||
export default class DevtoolsCtr extends ControllerModule {
|
||||
@ipcClientEvent('openDevtools')
|
||||
@IpcMethod()
|
||||
async openDevtools() {
|
||||
const devtoolsBrowser = this.app.browserManager.retrieveByIdentifier('devtools');
|
||||
devtoolsBrowser.show();
|
||||
|
||||
@@ -1,34 +1,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/infrastructure/IoCContainer';
|
||||
import { ShortcutActionType } from '@/shortcuts';
|
||||
|
||||
const ipcDecorator =
|
||||
(name: string, mode: 'client' | 'server') =>
|
||||
(target: any, methodName: string, descriptor?: any) => {
|
||||
const actions = IoCContainer.controllers.get(target.constructor) || [];
|
||||
actions.push({
|
||||
methodName,
|
||||
mode,
|
||||
name,
|
||||
});
|
||||
IoCContainer.controllers.set(target.constructor, actions);
|
||||
return descriptor;
|
||||
};
|
||||
|
||||
/**
|
||||
* IPC client event decorator for controllers
|
||||
*/
|
||||
export const ipcClientEvent = (method: keyof ClientDispatchEvents) =>
|
||||
ipcDecorator(method, 'client');
|
||||
|
||||
/**
|
||||
* IPC server event decorator for controllers
|
||||
*/
|
||||
export const ipcServerEvent = (method: keyof ServerDispatchEvents) =>
|
||||
ipcDecorator(method, 'server');
|
||||
import { IpcService } from '@/utils/ipc';
|
||||
|
||||
const shortcutDecorator = (name: string) => (target: any, methodName: string, descriptor?: any) => {
|
||||
const actions = IoCContainer.shortcuts.get(target.constructor) || [];
|
||||
@@ -68,10 +41,13 @@ interface IControllerModule {
|
||||
beforeAppReady?(): void;
|
||||
}
|
||||
|
||||
export class ControllerModule implements IControllerModule {
|
||||
export class ControllerModule extends IpcService implements IControllerModule {
|
||||
constructor(public app: App) {
|
||||
super();
|
||||
this.app = app;
|
||||
}
|
||||
}
|
||||
|
||||
export type IControlModule = typeof ControllerModule;
|
||||
|
||||
export { IpcMethod, IpcServerMethod } from '@/utils/ipc';
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { CreateServicesResult, IpcServiceConstructor, MergeIpcService } from '@/utils/ipc';
|
||||
|
||||
import AuthCtr from './AuthCtr';
|
||||
import BrowserWindowsCtr from './BrowserWindowsCtr';
|
||||
import DevtoolsCtr from './DevtoolsCtr';
|
||||
import LocalFileCtr from './LocalFileCtr';
|
||||
import McpInstallCtr from './McpInstallCtr';
|
||||
import MenuController from './MenuCtr';
|
||||
import NetworkProxyCtr from './NetworkProxyCtr';
|
||||
import NotificationCtr from './NotificationCtr';
|
||||
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
|
||||
import RemoteServerSyncCtr from './RemoteServerSyncCtr';
|
||||
import ShellCommandCtr from './ShellCommandCtr';
|
||||
import ShortcutController from './ShortcutCtr';
|
||||
import SystemController from './SystemCtr';
|
||||
import SystemServerCtr from './SystemServerCtr';
|
||||
import TrayMenuCtr from './TrayMenuCtr';
|
||||
import UpdaterCtr from './UpdaterCtr';
|
||||
import UploadFileCtr from './UploadFileCtr';
|
||||
import UploadFileServerCtr from './UploadFileServerCtr';
|
||||
|
||||
export const controllerIpcConstructors = [
|
||||
AuthCtr,
|
||||
BrowserWindowsCtr,
|
||||
DevtoolsCtr,
|
||||
LocalFileCtr,
|
||||
McpInstallCtr,
|
||||
MenuController,
|
||||
NetworkProxyCtr,
|
||||
NotificationCtr,
|
||||
RemoteServerConfigCtr,
|
||||
RemoteServerSyncCtr,
|
||||
ShellCommandCtr,
|
||||
ShortcutController,
|
||||
SystemController,
|
||||
TrayMenuCtr,
|
||||
UpdaterCtr,
|
||||
UploadFileCtr,
|
||||
] as const satisfies readonly IpcServiceConstructor[];
|
||||
|
||||
type DesktopControllerIpcConstructors = typeof controllerIpcConstructors;
|
||||
type DesktopControllerServices = CreateServicesResult<DesktopControllerIpcConstructors>;
|
||||
export type DesktopIpcServices = MergeIpcService<DesktopControllerServices>;
|
||||
|
||||
export const controllerServerIpcConstructors = [
|
||||
SystemServerCtr,
|
||||
UploadFileServerCtr,
|
||||
] as const satisfies readonly IpcServiceConstructor[];
|
||||
|
||||
type DesktopControllerServerConstructors = typeof controllerServerIpcConstructors;
|
||||
type DesktopServerControllerServices = CreateServicesResult<DesktopControllerServerConstructors>;
|
||||
export type DesktopServerIpcServices = MergeIpcService<DesktopServerControllerServices>;
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ElectronIPCEventHandler, ElectronIPCServer } from '@lobechat/electron-server-ipc';
|
||||
import { Session, app, ipcMain, protocol } from 'electron';
|
||||
import { Session, app, protocol } from 'electron';
|
||||
import { macOS, windows } from 'electron-is';
|
||||
import { pathExistsSync, remove } from 'fs-extra';
|
||||
import os from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { name } from '@/../../package.json';
|
||||
import { buildDir, LOCAL_DATABASE_DIR, nextStandaloneDir } from '@/const/dir';
|
||||
import { LOCAL_DATABASE_DIR, buildDir, nextStandaloneDir } from '@/const/dir';
|
||||
import { isDev } from '@/const/env';
|
||||
import { IControlModule } from '@/controllers';
|
||||
import { IServiceModule } from '@/services';
|
||||
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
||||
import { getServerMethodMetadata } from '@/utils/ipc';
|
||||
import { createLogger } from '@/utils/logger';
|
||||
import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
|
||||
|
||||
@@ -81,7 +81,7 @@ export class App {
|
||||
|
||||
// load controllers
|
||||
const controllers: IControlModule[] = importAll(
|
||||
(import.meta as any).glob('@/controllers/*Ctr.ts', { eager: true }),
|
||||
import.meta.glob('@/controllers/*Ctr.ts', { eager: true }),
|
||||
);
|
||||
|
||||
logger.debug(`Loading ${controllers.length} controllers`);
|
||||
@@ -89,13 +89,13 @@ export class App {
|
||||
|
||||
// load services
|
||||
const services: IServiceModule[] = importAll(
|
||||
(import.meta as any).glob('@/services/*Srv.ts', { eager: true }),
|
||||
import.meta.glob('@/services/*Srv.ts', { eager: true }),
|
||||
);
|
||||
|
||||
logger.debug(`Loading ${services.length} services`);
|
||||
services.forEach((service) => this.addService(service));
|
||||
|
||||
this.initializeIPCEvents();
|
||||
this.initializeServerIpcEvents();
|
||||
|
||||
this.i18n = new I18nManager(this);
|
||||
this.browserManager = new BrowserManager(this);
|
||||
@@ -268,10 +268,6 @@ export class App {
|
||||
private services = new Map<Class<any>, any>();
|
||||
|
||||
private ipcServer: ElectronIPCServer;
|
||||
/**
|
||||
* events dispatched from webview layer
|
||||
*/
|
||||
private ipcClientEventMap: IPCEventMap = new Map();
|
||||
private ipcServerEventMap: IPCEventMap = new Map();
|
||||
shortcutMethodMap: ShortcutMethodMap = new Map();
|
||||
protocolHandlerMap: ProtocolHandlerMap = new Map();
|
||||
@@ -327,22 +323,13 @@ export class App {
|
||||
const controller = new ControllerClass(this);
|
||||
this.controllers.set(ControllerClass, controller);
|
||||
|
||||
IoCContainer.controllers.get(ControllerClass)?.forEach((event) => {
|
||||
if (event.mode === 'client') {
|
||||
// Store all objects from event decorator in ipcClientEventMap
|
||||
this.ipcClientEventMap.set(event.name, {
|
||||
controller,
|
||||
methodName: event.methodName,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.mode === 'server') {
|
||||
// Store all objects from event decorator in ipcServerEventMap
|
||||
this.ipcServerEventMap.set(event.name, {
|
||||
controller,
|
||||
methodName: event.methodName,
|
||||
});
|
||||
}
|
||||
const serverMethods = getServerMethodMetadata(ControllerClass);
|
||||
serverMethods?.forEach((methodName, propertyKey) => {
|
||||
const channel = `${ControllerClass.groupName}.${methodName}`;
|
||||
this.ipcServerEventMap.set(channel, {
|
||||
controller,
|
||||
methodName: propertyKey,
|
||||
});
|
||||
});
|
||||
|
||||
IoCContainer.shortcuts.get(ControllerClass)?.forEach((shortcut) => {
|
||||
@@ -427,27 +414,8 @@ export class App {
|
||||
}
|
||||
}
|
||||
|
||||
private initializeIPCEvents() {
|
||||
logger.debug('Initializing IPC events');
|
||||
// Register batch controller client events for render side consumption
|
||||
this.ipcClientEventMap.forEach((eventInfo, key) => {
|
||||
const { controller, methodName } = eventInfo;
|
||||
|
||||
ipcMain.handle(key, async (e, data) => {
|
||||
// 从 WebContents 获取对应的 BrowserWindow id
|
||||
const senderIdentifier = this.browserManager.getIdentifierByWebContents(e.sender);
|
||||
try {
|
||||
return await controller[methodName](data, {
|
||||
identifier: senderIdentifier,
|
||||
} as IpcClientEventSender);
|
||||
} catch (error) {
|
||||
logger.error(`Error handling IPC event ${key}:`, error);
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Batch register server events from controllers for next server consumption
|
||||
private initializeServerIpcEvents() {
|
||||
logger.debug('Initializing IPC server events');
|
||||
const ipcServerEvents = {} as ElectronIPCEventHandler;
|
||||
|
||||
this.ipcServerEventMap.forEach((eventInfo, key) => {
|
||||
|
||||
@@ -5,6 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LOCAL_DATABASE_DIR } from '@/const/dir';
|
||||
|
||||
// Import after mocks are set up
|
||||
import { App } from '../App';
|
||||
|
||||
// Mock electron modules
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
@@ -24,6 +27,7 @@ vi.mock('electron', () => ({
|
||||
},
|
||||
ipcMain: {
|
||||
handle: vi.fn(),
|
||||
on: vi.fn(),
|
||||
},
|
||||
nativeTheme: {
|
||||
on: vi.fn(),
|
||||
@@ -166,9 +170,6 @@ vi.mock('@/utils/next-electron-rsc', () => ({
|
||||
vi.mock('../../controllers/*Ctr.ts', () => ({}));
|
||||
vi.mock('../../services/*Srv.ts', () => ({}));
|
||||
|
||||
// Import after mocks are set up
|
||||
import { App } from '../App';
|
||||
|
||||
describe('App - Database Lock Cleanup', () => {
|
||||
let appInstance: App;
|
||||
let mockLockPath: string;
|
||||
@@ -177,7 +178,7 @@ describe('App - Database Lock Cleanup', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock glob imports to return empty arrays
|
||||
(import.meta as any).glob = vi.fn(() => ({}));
|
||||
import.meta.glob = vi.fn(() => ({}));
|
||||
|
||||
mockLockPath = join('/mock/storage/path', LOCAL_DATABASE_DIR) + '.lock';
|
||||
});
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
* 存储应用中需要用装饰器的类
|
||||
*/
|
||||
export class IoCContainer {
|
||||
static controllers: WeakMap<
|
||||
any,
|
||||
{ methodName: string; mode: 'client' | 'server'; name: string }[]
|
||||
> = new WeakMap();
|
||||
|
||||
static shortcuts: WeakMap<any, { methodName: string; name: string }[]> = new WeakMap();
|
||||
|
||||
static protocolHandlers: WeakMap<any, { action: string; methodName: string; urlType: string }[]> =
|
||||
|
||||
@@ -13,52 +13,6 @@ describe('IoCContainer', () => {
|
||||
// For each test, use fresh class instances
|
||||
});
|
||||
|
||||
describe('controllers WeakMap', () => {
|
||||
it('should store controller metadata', () => {
|
||||
const metadata = [{ methodName: 'handleMessage', mode: 'client' as const, name: 'message' }];
|
||||
|
||||
IoCContainer.controllers.set(TestController, metadata);
|
||||
|
||||
expect(IoCContainer.controllers.get(TestController)).toEqual(metadata);
|
||||
});
|
||||
|
||||
it('should allow multiple controllers', () => {
|
||||
const metadata1 = [{ methodName: 'method1', mode: 'client' as const, name: 'action1' }];
|
||||
const metadata2 = [{ methodName: 'method2', mode: 'server' as const, name: 'action2' }];
|
||||
|
||||
IoCContainer.controllers.set(TestController, metadata1);
|
||||
IoCContainer.controllers.set(AnotherController, metadata2);
|
||||
|
||||
expect(IoCContainer.controllers.get(TestController)).toEqual(metadata1);
|
||||
expect(IoCContainer.controllers.get(AnotherController)).toEqual(metadata2);
|
||||
});
|
||||
|
||||
it('should allow overwriting controller metadata', () => {
|
||||
const oldMetadata = [{ methodName: 'oldMethod', mode: 'client' as const, name: 'old' }];
|
||||
const newMetadata = [{ methodName: 'newMethod', mode: 'server' as const, name: 'new' }];
|
||||
|
||||
IoCContainer.controllers.set(TestController, oldMetadata);
|
||||
IoCContainer.controllers.set(TestController, newMetadata);
|
||||
|
||||
expect(IoCContainer.controllers.get(TestController)).toEqual(newMetadata);
|
||||
});
|
||||
|
||||
it('should support multiple methods per controller', () => {
|
||||
const metadata = [
|
||||
{ methodName: 'method1', mode: 'client' as const, name: 'action1' },
|
||||
{ methodName: 'method2', mode: 'server' as const, name: 'action2' },
|
||||
{ methodName: 'method3', mode: 'client' as const, name: 'action3' },
|
||||
];
|
||||
|
||||
IoCContainer.controllers.set(TestController, metadata);
|
||||
|
||||
const stored = IoCContainer.controllers.get(TestController);
|
||||
expect(stored).toHaveLength(3);
|
||||
expect(stored?.[0].mode).toBe('client');
|
||||
expect(stored?.[1].mode).toBe('server');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shortcuts WeakMap', () => {
|
||||
it('should store shortcut metadata', () => {
|
||||
const metadata = [{ methodName: 'toggleDarkMode', name: 'CmdOrCtrl+Shift+D' }];
|
||||
@@ -141,10 +95,6 @@ describe('IoCContainer', () => {
|
||||
});
|
||||
|
||||
describe('static properties', () => {
|
||||
it('should have controllers as a WeakMap', () => {
|
||||
expect(IoCContainer.controllers).toBeInstanceOf(WeakMap);
|
||||
});
|
||||
|
||||
it('should have shortcuts as a WeakMap', () => {
|
||||
expect(IoCContainer.shortcuts).toBeInstanceOf(WeakMap);
|
||||
});
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import type { DesktopIpcServices, DesktopServerIpcServices } from './controllers/registry';
|
||||
|
||||
declare module '@lobechat/electron-client-ipc' {
|
||||
interface DesktopIpcServicesMap extends DesktopIpcServices {}
|
||||
}
|
||||
|
||||
export type { DesktopIpcServices, DesktopServerIpcServices };
|
||||
@@ -0,0 +1,2 @@
|
||||
// Export types for renderer/server to use
|
||||
export type { DesktopIpcServices, DesktopServerIpcServices } from './controllers/registry';
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import 'vite/client';
|
||||
|
||||
export {};
|
||||
@@ -17,6 +17,12 @@ const repoRoot = path.resolve(__dirname, '../../../../..');
|
||||
|
||||
describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integration', () => {
|
||||
const searchService = new MacOSSearchServiceImpl();
|
||||
const ensureResults = (results: unknown[], context: string) => {
|
||||
if (results.length > 0) return true;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`⚠️ Spotlight returned 0 results for ${context} - indexing may be incomplete`);
|
||||
return false;
|
||||
};
|
||||
|
||||
describe('checkSearchServiceStatus', () => {
|
||||
it('should verify Spotlight is available on macOS', async () => {
|
||||
@@ -34,7 +40,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
if (!ensureResults(results, 'package.json search')) return;
|
||||
|
||||
// Should find at least one package.json
|
||||
const packageJson = results.find((r) => r.name === 'package.json');
|
||||
@@ -49,7 +55,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
limit: 10,
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
if (!ensureResults(results, 'README search')) return;
|
||||
|
||||
// Should contain markdown files
|
||||
const mdFile = results.find((r) => r.type === 'md');
|
||||
@@ -64,7 +70,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
if (!ensureResults(results, 'TypeScript file search')) return;
|
||||
|
||||
// Should find the macOS.ts implementation file
|
||||
const macOSFile = results.find((r) => r.name.includes('macOS') && r.type === 'ts');
|
||||
@@ -106,7 +112,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
if (!ensureResults(results, 'test file search')) return;
|
||||
|
||||
// Should find test files (can be in __tests__ directory or co-located with source files)
|
||||
const testFile = results.find((r) => r.name.endsWith('.test.ts'));
|
||||
@@ -161,6 +167,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
|
||||
if (!ensureResults(results, 'TypeScript identification')) return;
|
||||
const tsFile = results.find((r) => r.name === 'LocalFileCtr.ts');
|
||||
if (tsFile) {
|
||||
expect(tsFile.type).toBe('ts');
|
||||
@@ -176,6 +183,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
|
||||
if (!ensureResults(results, 'JSON identification')) return;
|
||||
const jsonFile = results.find((r) => r.name.includes('tsconfig') && r.type === 'json');
|
||||
if (jsonFile) {
|
||||
expect(jsonFile.type).toBe('json');
|
||||
@@ -191,6 +199,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
|
||||
if (!ensureResults(results, 'directory identification')) return;
|
||||
const testDir = results.find((r) => r.name === '__tests__' && r.isDirectory);
|
||||
if (testDir) {
|
||||
expect(testDir.isDirectory).toBe(true);
|
||||
@@ -221,7 +230,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
if (!ensureResults(results, 'file metadata read')) return;
|
||||
|
||||
const file = results[0];
|
||||
|
||||
@@ -279,7 +288,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
onlyIn: repoRoot,
|
||||
});
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
if (!ensureResults(results, 'fuzzy search accuracy')) return;
|
||||
|
||||
// Should find LocalFileCtr.ts or similar files
|
||||
const found = results.some(
|
||||
@@ -319,8 +328,8 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
||||
});
|
||||
|
||||
// Both searches should find similar files
|
||||
expect(lowerResults.length).toBeGreaterThan(0);
|
||||
expect(upperResults.length).toBeGreaterThan(0);
|
||||
if (!ensureResults(lowerResults, 'case-insensitive search (lower)')) return;
|
||||
if (!ensureResults(upperResults, 'case-insensitive search (upper)')) return;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@lobehub/desktop-ipc-typings",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./exports.d.ts",
|
||||
"types": "./exports.d.ts",
|
||||
"exports": {
|
||||
".": "./exports.d.ts"
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ interface UploadFileParams {
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface FileMetadata {
|
||||
export interface FileMetadata {
|
||||
date: string;
|
||||
dirname: string;
|
||||
filename: string;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface IpcClientEventSender {
|
||||
identifier: string;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { IpcContext } from '../base';
|
||||
import {
|
||||
IpcMethod,
|
||||
IpcServerMethod,
|
||||
IpcService,
|
||||
getIpcContext,
|
||||
getServerMethodMetadata,
|
||||
} from '../base';
|
||||
|
||||
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
||||
ipcMainHandleMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: ipcMainHandleMock,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ipc service base', () => {
|
||||
beforeEach(() => {
|
||||
ipcMainHandleMock.mockClear();
|
||||
});
|
||||
|
||||
it('registers handlers and forwards payload/context correctly', async () => {
|
||||
class TestService extends IpcService {
|
||||
static readonly groupName = 'test';
|
||||
public lastCall: { payload: string | undefined; context?: IpcContext } | null = null;
|
||||
|
||||
@IpcMethod()
|
||||
ping(payload?: string) {
|
||||
this.lastCall = { context: getIpcContext(), payload };
|
||||
return 'pong';
|
||||
}
|
||||
}
|
||||
|
||||
const service = new TestService();
|
||||
|
||||
expect(service).toBeTruthy();
|
||||
expect(ipcMainHandleMock).toHaveBeenCalledWith('test.ping', expect.any(Function));
|
||||
|
||||
const handler = ipcMainHandleMock.mock.calls[0][1];
|
||||
const fakeSender = { id: 1 } as any;
|
||||
const fakeEvent = { sender: fakeSender } as any;
|
||||
|
||||
const result = await handler(fakeEvent, 'hello');
|
||||
|
||||
expect(result).toBe('pong');
|
||||
expect(service.lastCall).toEqual({
|
||||
context: { event: fakeEvent, sender: fakeSender },
|
||||
payload: 'hello',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows direct method invocation without IPC context', () => {
|
||||
class DirectCallService extends IpcService {
|
||||
static readonly groupName = 'direct';
|
||||
public invokedWith: string | null = null;
|
||||
|
||||
@IpcMethod()
|
||||
run(payload: string) {
|
||||
this.invokedWith = payload;
|
||||
return payload.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
const service = new DirectCallService();
|
||||
const result = service.run('test');
|
||||
|
||||
expect(result).toBe('TEST');
|
||||
expect(service.invokedWith).toBe('test');
|
||||
expect(ipcMainHandleMock).toHaveBeenCalledWith('direct.run', expect.any(Function));
|
||||
});
|
||||
|
||||
it('collects server method metadata for decorators', () => {
|
||||
class ServerService extends IpcService {
|
||||
static readonly groupName = 'server';
|
||||
|
||||
@IpcServerMethod()
|
||||
fetch(_: string) {
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = getServerMethodMetadata(ServerService);
|
||||
expect(metadata).toBeDefined();
|
||||
expect(metadata?.get('fetch')).toBe('fetch');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { IpcMainInvokeEvent, WebContents } from 'electron';
|
||||
import { ipcMain } from 'electron';
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
|
||||
// Base context for IPC methods
|
||||
export interface IpcContext {
|
||||
event: IpcMainInvokeEvent;
|
||||
sender: WebContents;
|
||||
}
|
||||
|
||||
// Metadata storage for decorated methods
|
||||
const methodMetadata = new WeakMap<any, Map<string, string>>();
|
||||
const serverMethodMetadata = new WeakMap<any, Map<string, string>>();
|
||||
const ipcContextStorage = new AsyncLocalStorage<IpcContext>();
|
||||
|
||||
// Decorator for IPC methods
|
||||
export function IpcMethod(channelName?: string) {
|
||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const { constructor } = target;
|
||||
|
||||
if (!methodMetadata.has(constructor)) {
|
||||
methodMetadata.set(constructor, new Map());
|
||||
}
|
||||
|
||||
const methods = methodMetadata.get(constructor)!;
|
||||
methods.set(propertyKey, channelName || propertyKey);
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
export function IpcServerMethod(channelName?: string) {
|
||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const { constructor } = target;
|
||||
|
||||
if (!serverMethodMetadata.has(constructor)) {
|
||||
serverMethodMetadata.set(constructor, new Map());
|
||||
}
|
||||
|
||||
const methods = serverMethodMetadata.get(constructor)!;
|
||||
methods.set(propertyKey, channelName || propertyKey);
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
// Handler registry for IPC methods
|
||||
export class IpcHandler {
|
||||
private static instance: IpcHandler;
|
||||
private registeredChannels = new Set<string>();
|
||||
|
||||
static getInstance(): IpcHandler {
|
||||
if (!IpcHandler.instance) {
|
||||
IpcHandler.instance = new IpcHandler();
|
||||
}
|
||||
return IpcHandler.instance;
|
||||
}
|
||||
|
||||
registerMethod<TArgs extends unknown[], TOutput>(
|
||||
channel: string,
|
||||
handler: (...args: TArgs) => Promise<TOutput> | TOutput,
|
||||
) {
|
||||
if (this.registeredChannels.has(channel)) {
|
||||
return; // Already registered
|
||||
}
|
||||
|
||||
this.registeredChannels.add(channel);
|
||||
|
||||
ipcMain.handle(channel, async (event: IpcMainInvokeEvent, ...args: any[]) => {
|
||||
const context: IpcContext = {
|
||||
event,
|
||||
sender: event.sender,
|
||||
};
|
||||
|
||||
return ipcContextStorage.run(context, async () => {
|
||||
try {
|
||||
const typedArgs = args as TArgs;
|
||||
return await handler(...typedArgs);
|
||||
} catch (error) {
|
||||
console.error(`Error in IPC method ${channel}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Send events to renderer
|
||||
sendToRenderer<T = any>(webContents: WebContents, channel: string, data: T) {
|
||||
webContents.send(channel, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Base class for IPC service groups
|
||||
export abstract class IpcService {
|
||||
protected handler = IpcHandler.getInstance();
|
||||
static readonly groupName: string;
|
||||
|
||||
constructor() {
|
||||
this.registerMethods();
|
||||
}
|
||||
|
||||
protected registerMethods(): void {
|
||||
const { constructor } = this;
|
||||
const methods = methodMetadata.get(constructor);
|
||||
|
||||
if (methods) {
|
||||
methods.forEach((methodName, propertyKey) => {
|
||||
const method = (this as any)[propertyKey];
|
||||
if (typeof method === 'function') {
|
||||
this.registerMethod(methodName, method.bind(this));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected registerMethod<TArgs extends unknown[], TOutput>(
|
||||
methodName: string,
|
||||
handler: (...args: TArgs) => Promise<TOutput> | TOutput,
|
||||
) {
|
||||
const groupName = (this.constructor as typeof IpcService).groupName;
|
||||
const channel = `${groupName}.${methodName}`;
|
||||
this.handler.registerMethod(channel, handler);
|
||||
}
|
||||
}
|
||||
|
||||
// Service constructor with groupName
|
||||
export interface IpcServiceConstructor {
|
||||
new (...args: any[]): IpcService;
|
||||
readonly groupName: string;
|
||||
}
|
||||
|
||||
// Create services function that infers types from service constructors
|
||||
export function createServices<T extends readonly IpcServiceConstructor[]>(
|
||||
serviceConstructors: T,
|
||||
...constructorArgs: any[]
|
||||
): CreateServicesResult<T> {
|
||||
const services = {} as any;
|
||||
|
||||
for (const ServiceConstructor of serviceConstructors) {
|
||||
const instance = new ServiceConstructor(...constructorArgs);
|
||||
const groupName = ServiceConstructor.groupName;
|
||||
|
||||
if (!groupName) {
|
||||
throw new Error(
|
||||
`Service ${ServiceConstructor.name} must define a static readonly groupName property`,
|
||||
);
|
||||
}
|
||||
|
||||
services[groupName] = instance;
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
// Helper type for createServices return type
|
||||
export type CreateServicesResult<T extends readonly IpcServiceConstructor[]> = {
|
||||
[K in T[number] as K['groupName']]: InstanceType<K>;
|
||||
};
|
||||
|
||||
export function getServerMethodMetadata(target: IpcServiceConstructor) {
|
||||
return serverMethodMetadata.get(target);
|
||||
}
|
||||
|
||||
export function getIpcContext() {
|
||||
return ipcContextStorage.getStore();
|
||||
}
|
||||
|
||||
export function runWithIpcContext<T>(context: IpcContext, callback: () => T): T {
|
||||
return ipcContextStorage.run(context, callback);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export type { CreateServicesResult, IpcContext, IpcServiceConstructor } from './base';
|
||||
export {
|
||||
createServices,
|
||||
getIpcContext,
|
||||
getServerMethodMetadata,
|
||||
IpcMethod,
|
||||
IpcServerMethod,
|
||||
IpcService,
|
||||
runWithIpcContext,
|
||||
} from './base';
|
||||
export type { ExtractServiceMethods,MergeIpcService } from './utility';
|
||||
@@ -0,0 +1,20 @@
|
||||
// Extract method signatures from service classes
|
||||
type ExtractMethodSignature<T> = T extends (...args: infer Args) => infer Output
|
||||
? (...args: Args) => AlwaysPromise<Output>
|
||||
: never;
|
||||
|
||||
export type ExtractServiceMethods<T> = {
|
||||
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: ExtractMethodSignature<T[K]>;
|
||||
};
|
||||
|
||||
type AlwaysPromise<T> = Promise<Awaited<T>>;
|
||||
|
||||
// TypeScript utility type to automatically merge IPC services
|
||||
// This version works with both the old object format and new createServices format
|
||||
export type MergeIpcService<T> = {
|
||||
[K in keyof T]: T[K] extends new (...args: any[]) => infer Instance
|
||||
? ExtractServiceMethods<Instance>
|
||||
: T[K] extends infer Instance
|
||||
? ExtractServiceMethods<Instance>
|
||||
: never;
|
||||
};
|
||||
@@ -15,5 +15,8 @@ export const setupElectronApi = () => {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', { invoke, onStreamInvoke });
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
invoke,
|
||||
onStreamInvoke,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ClientDispatchEventKey } from '@lobechat/electron-client-ipc';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock electron module
|
||||
@@ -21,9 +20,9 @@ describe('invoke', () => {
|
||||
const expectedResult = { success: true };
|
||||
mockIpcRendererInvoke.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await invoke('getAppVersion' as ClientDispatchEventKey);
|
||||
const result = await invoke('system.getAppVersion');
|
||||
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('getAppVersion');
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('system.getAppVersion');
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
@@ -33,9 +32,9 @@ describe('invoke', () => {
|
||||
const expectedResult = { navigated: true };
|
||||
mockIpcRendererInvoke.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await invoke('interceptRoute' as ClientDispatchEventKey, eventData);
|
||||
const result = await invoke('windows.interceptRoute', eventData);
|
||||
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('interceptRoute', eventData);
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('windows.interceptRoute', eventData);
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
@@ -59,16 +58,14 @@ describe('invoke', () => {
|
||||
const error = new Error('IPC communication failed');
|
||||
mockIpcRendererInvoke.mockRejectedValue(error);
|
||||
|
||||
await expect(invoke('getAppVersion' as ClientDispatchEventKey)).rejects.toThrow(
|
||||
'IPC communication failed',
|
||||
);
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('getAppVersion');
|
||||
await expect(invoke('system.getAppVersion')).rejects.toThrow('IPC communication failed');
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('system.getAppVersion');
|
||||
});
|
||||
|
||||
it('should handle ipcRenderer returning undefined', async () => {
|
||||
mockIpcRendererInvoke.mockResolvedValue(undefined);
|
||||
|
||||
const result = await invoke('someEvent' as ClientDispatchEventKey);
|
||||
const result = await invoke('someEvent');
|
||||
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('someEvent');
|
||||
expect(result).toBeUndefined();
|
||||
@@ -77,7 +74,7 @@ describe('invoke', () => {
|
||||
it('should handle ipcRenderer returning null', async () => {
|
||||
mockIpcRendererInvoke.mockResolvedValue(null);
|
||||
|
||||
const result = await invoke('someEvent' as ClientDispatchEventKey);
|
||||
const result = await invoke('someEvent');
|
||||
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('someEvent');
|
||||
expect(result).toBeNull();
|
||||
@@ -96,7 +93,7 @@ describe('invoke', () => {
|
||||
};
|
||||
mockIpcRendererInvoke.mockResolvedValue(complexData);
|
||||
|
||||
const result = await invoke('getData' as ClientDispatchEventKey);
|
||||
const result = await invoke('getData');
|
||||
|
||||
expect(result).toEqual(complexData);
|
||||
});
|
||||
@@ -125,9 +122,9 @@ describe('invoke', () => {
|
||||
.mockResolvedValueOnce({ id: 3 });
|
||||
|
||||
const [result1, result2, result3] = await Promise.all([
|
||||
invoke('event1' as ClientDispatchEventKey),
|
||||
invoke('event2' as ClientDispatchEventKey),
|
||||
invoke('event3' as ClientDispatchEventKey),
|
||||
invoke('event1'),
|
||||
invoke('event2'),
|
||||
invoke('event3'),
|
||||
]);
|
||||
|
||||
expect(result1).toEqual({ id: 1 });
|
||||
@@ -139,7 +136,7 @@ describe('invoke', () => {
|
||||
it('should handle empty string as data parameter', async () => {
|
||||
mockIpcRendererInvoke.mockResolvedValue({ received: '' });
|
||||
|
||||
const result = await invoke('sendData' as ClientDispatchEventKey, '');
|
||||
const result = await invoke('sendData', '');
|
||||
|
||||
expect(mockIpcRendererInvoke).toHaveBeenCalledWith('sendData', '');
|
||||
expect(result).toEqual({ received: '' });
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { ClientDispatchEventKey, DispatchInvoke } from '@lobechat/electron-client-ipc';
|
||||
import { DispatchInvoke } from '@lobechat/electron-client-ipc';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
/**
|
||||
* Client-side method to invoke electron main process
|
||||
*/
|
||||
export const invoke: DispatchInvoke = async <T extends ClientDispatchEventKey>(
|
||||
event: T,
|
||||
...data: any[]
|
||||
) => ipcRenderer.invoke(event, ...data);
|
||||
export const invoke: DispatchInvoke = async (event, ...data) => ipcRenderer.invoke(event, ...data);
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('setupRouteInterceptors', () => {
|
||||
const externalUrl = 'https://google.com';
|
||||
const result = window.open(externalUrl, '_blank');
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith('openExternalLink', externalUrl);
|
||||
expect(invoke).toHaveBeenCalledWith('system.openExternalLink', externalUrl);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('setupRouteInterceptors', () => {
|
||||
const externalUrl = new URL('https://github.com');
|
||||
const result = window.open(externalUrl, '_blank');
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith('openExternalLink', 'https://github.com/');
|
||||
expect(invoke).toHaveBeenCalledWith('system.openExternalLink', 'https://github.com/');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('setupRouteInterceptors', () => {
|
||||
// We can't fully test the original behavior in happy-dom, but we can verify invoke is not called
|
||||
window.open(internalUrl);
|
||||
|
||||
expect(invoke).not.toHaveBeenCalledWith('openExternalLink', expect.anything());
|
||||
expect(invoke).not.toHaveBeenCalledWith('system.openExternalLink', expect.anything());
|
||||
});
|
||||
|
||||
it('should handle relative URL that resolves as internal link', () => {
|
||||
@@ -81,7 +81,7 @@ describe('setupRouteInterceptors', () => {
|
||||
window.open(relativeUrl);
|
||||
|
||||
// Since it's internal, it won't call invoke for external link
|
||||
expect(invoke).not.toHaveBeenCalledWith('openExternalLink', expect.anything());
|
||||
expect(invoke).not.toHaveBeenCalledWith('system.openExternalLink', expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('setupRouteInterceptors', () => {
|
||||
// Wait for async handling
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith('openExternalLink', 'https://example.com/');
|
||||
expect(invoke).toHaveBeenCalledWith('system.openExternalLink', 'https://example.com/');
|
||||
expect(preventDefaultSpy).toHaveBeenCalled();
|
||||
expect(stopPropagationSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -129,7 +129,7 @@ describe('setupRouteInterceptors', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
|
||||
expect(invoke).toHaveBeenCalledWith('interceptRoute', {
|
||||
expect(invoke).toHaveBeenCalledWith('windows.interceptRoute', {
|
||||
path: '/desktop/devtools',
|
||||
source: 'link-click',
|
||||
url: 'http://localhost:3000/desktop/devtools',
|
||||
@@ -166,7 +166,7 @@ describe('setupRouteInterceptors', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(preventDefaultSpy).not.toHaveBeenCalled();
|
||||
expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
|
||||
expect(invoke).not.toHaveBeenCalledWith('windows.interceptRoute', expect.anything());
|
||||
});
|
||||
|
||||
it('should handle non-HTTP link protocols as external links', async () => {
|
||||
@@ -184,7 +184,7 @@ describe('setupRouteInterceptors', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
// mailto: links are treated as external links by the URL constructor
|
||||
expect(invoke).toHaveBeenCalledWith('openExternalLink', 'mailto:test@example.com');
|
||||
expect(invoke).toHaveBeenCalledWith('system.openExternalLink', 'mailto:test@example.com');
|
||||
expect(preventDefaultSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -205,7 +205,7 @@ describe('setupRouteInterceptors', () => {
|
||||
history.pushState({}, '', '/desktop/devtools');
|
||||
|
||||
expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
|
||||
expect(invoke).toHaveBeenCalledWith('interceptRoute', {
|
||||
expect(invoke).toHaveBeenCalledWith('windows.interceptRoute', {
|
||||
path: '/desktop/devtools',
|
||||
source: 'push-state',
|
||||
url: 'http://localhost:3000/desktop/devtools',
|
||||
@@ -245,7 +245,7 @@ describe('setupRouteInterceptors', () => {
|
||||
|
||||
history.pushState({}, '', '/chat/new');
|
||||
|
||||
expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
|
||||
expect(invoke).not.toHaveBeenCalledWith('windows.interceptRoute', expect.anything());
|
||||
});
|
||||
|
||||
it('should handle pushState errors gracefully', () => {
|
||||
@@ -279,7 +279,7 @@ describe('setupRouteInterceptors', () => {
|
||||
history.replaceState({}, '', '/desktop/devtools');
|
||||
|
||||
expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
|
||||
expect(invoke).toHaveBeenCalledWith('interceptRoute', {
|
||||
expect(invoke).toHaveBeenCalledWith('windows.interceptRoute', {
|
||||
path: '/desktop/devtools',
|
||||
source: 'replace-state',
|
||||
url: 'http://localhost:3000/desktop/devtools',
|
||||
@@ -317,7 +317,7 @@ describe('setupRouteInterceptors', () => {
|
||||
|
||||
history.replaceState({}, '', '/chat/session-123');
|
||||
|
||||
expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
|
||||
expect(invoke).not.toHaveBeenCalledWith('windows.interceptRoute', expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -385,7 +385,7 @@ describe('setupRouteInterceptors', () => {
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith('interceptRoute', {
|
||||
expect(invoke).toHaveBeenCalledWith('windows.interceptRoute', {
|
||||
path: '/desktop/devtools',
|
||||
source: 'push-state',
|
||||
url: 'http://localhost:3000/desktop/devtools',
|
||||
|
||||
@@ -11,7 +11,7 @@ const interceptRoute = async (
|
||||
|
||||
// Use electron-client-ipc's dispatch method
|
||||
try {
|
||||
await invoke('interceptRoute', { path, source, url });
|
||||
await invoke('windows.interceptRoute', { path, source, url });
|
||||
} catch (e) {
|
||||
console.error(`[preload] Route interception (${source}) call failed`, e);
|
||||
}
|
||||
@@ -37,14 +37,14 @@ export const setupRouteInterceptors = function () {
|
||||
if (urlObj.origin !== window.location.origin) {
|
||||
console.log(`[preload] Intercepted window.open for external URL:`, urlString);
|
||||
// Call main process to handle external link
|
||||
invoke('openExternalLink', urlString);
|
||||
invoke('system.openExternalLink', urlString);
|
||||
return null; // Return null to indicate no window was opened
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle invalid URL or special protocol
|
||||
console.error(`[preload] Intercepted window.open for special protocol:`, url);
|
||||
console.error(error);
|
||||
invoke('openExternalLink', typeof url === 'string' ? url : url.toString());
|
||||
invoke('system.openExternalLink', typeof url === 'string' ? url : url.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export const setupRouteInterceptors = function () {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Call main process to handle external link
|
||||
await invoke('openExternalLink', url.href);
|
||||
await invoke('system.openExternalLink', url.href);
|
||||
return false; // Explicitly prevent subsequent processing
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,28 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"target": "ESNext",
|
||||
"emitDeclarationOnly": true,
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"experimentalDecorators": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@/*": ["src/main/*"],
|
||||
"~common/*": ["src/common/*"]
|
||||
"@/*": [
|
||||
"src/main/*"
|
||||
],
|
||||
"~common/*": [
|
||||
"src/common/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src/main/**/*", "src/preload/**/*", "electron-builder.js"]
|
||||
}
|
||||
"include": [
|
||||
"src/main/**/*",
|
||||
"src/preload/**/*",
|
||||
"electron-builder.js"
|
||||
]
|
||||
}
|
||||
@@ -1,18 +1,4 @@
|
||||
[
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update link handling in PlanTag component to use react-router-dom."]
|
||||
},
|
||||
"date": "2025-12-08",
|
||||
"version": "2.0.0-next.164"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Add smooth scroll to top on 'More' button click in Title component."]
|
||||
},
|
||||
"date": "2025-12-06",
|
||||
"version": "2.0.0-next.163"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-12-05",
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
},
|
||||
"betterAuth": {
|
||||
"errors": {
|
||||
"confirmPasswordRequired": "يرجى تأكيد كلمة المرور",
|
||||
"emailExists": "هذا البريد الإلكتروني مسجّل بالفعل، يرجى تسجيل الدخول مباشرة",
|
||||
"emailInvalid": "يرجى إدخال عنوان بريد إلكتروني صالح",
|
||||
"emailNotRegistered": "هذا البريد الإلكتروني غير مسجل",
|
||||
@@ -66,7 +65,6 @@
|
||||
"passwordFormat": "يجب أن تحتوي كلمة المرور على أحرف وأرقام",
|
||||
"passwordMaxLength": "يجب ألا تتجاوز كلمة المرور 64 حرفًا",
|
||||
"passwordMinLength": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل",
|
||||
"passwordMismatch": "كلمتا المرور غير متطابقتين",
|
||||
"passwordRequired": "يرجى إدخال كلمة المرور",
|
||||
"usernameNotRegistered": "اسم المستخدم هذا غير مسجل",
|
||||
"usernameRequired": "يرجى إدخال اسم المستخدم"
|
||||
@@ -127,7 +125,6 @@
|
||||
"submit": "تسجيل الدخول"
|
||||
},
|
||||
"signup": {
|
||||
"confirmPasswordPlaceholder": "يرجى تأكيد كلمة المرور",
|
||||
"emailPlaceholder": "يرجى إدخال عنوان البريد الإلكتروني",
|
||||
"error": "فشل التسجيل، يرجى المحاولة مرة أخرى",
|
||||
"firstNamePlaceholder": "الاسم الأول",
|
||||
|
||||
@@ -3,22 +3,6 @@
|
||||
"title": "النموذج"
|
||||
},
|
||||
"active": "نشط",
|
||||
"agentBuilder": {
|
||||
"installPlugin": {
|
||||
"authRequired": "يتطلب مكون MCP السحابي تسجيل الدخول والمصادقة",
|
||||
"cancel": "إلغاء",
|
||||
"clickApproveToConnect": "انقر على \"الموافقة\" للاتصال وتفويض هذا التكامل",
|
||||
"connectedAndEnabled": "تم الاتصال والتفعيل",
|
||||
"connectionFailed": "فشل الاتصال",
|
||||
"installFailed": "فشل التثبيت",
|
||||
"installPlugin": "تثبيت المكون الإضافي",
|
||||
"installToEnable": "قم بتثبيت هذا المكون الإضافي لتمكين المساعد",
|
||||
"installedAndEnabled": "تم التثبيت والتفعيل",
|
||||
"requiresAuth": "يتطلب تفويضًا، انقر على \"الموافقة\" للاتصال",
|
||||
"retry": "إعادة المحاولة"
|
||||
},
|
||||
"welcome": "مرحبًا، أنا **Lobe AI**، خبير إعداد مساعدك الشخصي. أخبرني بنوع المساعد الذي تريده، وسأقوم بإعداده لك."
|
||||
},
|
||||
"agentDefaultMessage": "مرحبًا، أنا **{{name}}**، يمكنك بدء المحادثة معي على الفور، أو يمكنك الذهاب إلى [إعدادات المساعد]({{url}}) لإكمال معلوماتي.",
|
||||
"agentDefaultMessageWithSystemRole": "مرحبًا، أنا **{{name}}**، كيف يمكنني مساعدتك؟",
|
||||
"agentDefaultMessageWithoutEdit": "مرحبًا، أنا **{{name}}**، كيف يمكنني مساعدتك؟",
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"title": "سجل الإصدارات"
|
||||
}
|
||||
},
|
||||
"downloads": "عدد التنزيلات",
|
||||
"list": "قائمة المساعدين",
|
||||
"marketSource": {
|
||||
"label": "تبديل مصدر المجتمع",
|
||||
@@ -693,18 +692,6 @@
|
||||
"home": "الصفحة الرئيسية",
|
||||
"model": "النموذج",
|
||||
"plugin": "الإضافة",
|
||||
"provider": "مزود النموذج",
|
||||
"user": "المستخدم"
|
||||
},
|
||||
"user": {
|
||||
"agents": "المساعدون",
|
||||
"downloads": "التنزيلات",
|
||||
"editProfile": "تعديل الملف الشخصي",
|
||||
"login": "تسجيل الدخول",
|
||||
"logout": "تسجيل الخروج",
|
||||
"myProfile": "صفحتي الشخصية",
|
||||
"noAgents": "لم يقم هذا المستخدم بنشر أي مساعدين بعد",
|
||||
"publishedAgents": "المساعدون المنشورون",
|
||||
"website": "الموقع الشخصي"
|
||||
"provider": "مزود النموذج"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,20 +86,6 @@
|
||||
},
|
||||
"newFolder": "إنشاء مجلد جديد",
|
||||
"newPage": "مستند جديد",
|
||||
"notion": {
|
||||
"error": "فشل في استيراد ملف Notion",
|
||||
"foundFiles": "تم العثور على {{count}} ملف",
|
||||
"importing": "جارٍ استيراد ملفات Notion...",
|
||||
"noMarkdownFiles": "لم يتم العثور على ملفات Markdown في ملف ZIP",
|
||||
"partial": "تم استيراد {{success}} ملفًا بنجاح، وفشل {{failed}} ملفًا",
|
||||
"success": "تم استيراد {{count}} ملفًا بنجاح"
|
||||
},
|
||||
"notionGuide": {
|
||||
"cancel": "إلغاء الاستيراد الآن",
|
||||
"desc": "يرجى أولاً تصدير ملفات Markdown (بصيغة ZIP) من Notion، ثم النقر على متابعة لاختيار ملف الضغط واستيراد جميع الصفحات.",
|
||||
"ok": "اختر ملف ZIP من Notion",
|
||||
"title": "استيراد محتوى Notion"
|
||||
},
|
||||
"uploadFile": "رفع ملف",
|
||||
"uploadFolder": "رفع مجلد"
|
||||
},
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"starter": {
|
||||
"createAgent": "إنشاء مساعد",
|
||||
"deepResearch": "بحث معمق",
|
||||
"developing": "قيد التطوير",
|
||||
"image": "رسم",
|
||||
"write": "كتابة"
|
||||
}
|
||||
}
|
||||
@@ -70,12 +70,12 @@
|
||||
"title": "تبديل المساعد بسرعة"
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"desc": "إظهار أو إخفاء اللوحة الجانبية اليسرى",
|
||||
"title": "إظهار/إخفاء اللوحة الجانبية اليسرى"
|
||||
"desc": "عرض أو إخفاء لوحة المساعد على اليسار",
|
||||
"title": "عرض/إخفاء لوحة المساعد"
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"desc": "إظهار أو إخفاء اللوحة الجانبية اليمنى",
|
||||
"title": "إظهار/إخفاء اللوحة الجانبية اليمنى"
|
||||
"desc": "عرض أو إخفاء لوحة المواضيع على اليمين",
|
||||
"title": "عرض/إخفاء لوحة الموضوع"
|
||||
},
|
||||
"toggleZenMode": {
|
||||
"desc": "في وضع التركيز، عرض المحادثة الحالية فقط، وإخفاء واجهة المستخدم الأخرى",
|
||||
|
||||
@@ -52,42 +52,5 @@
|
||||
"submit": "تم التفويض بنجاح! يمكنك الآن نشر المساعد.",
|
||||
"upload": "تم التفويض بنجاح! يمكنك الآن نشر إصدار جديد."
|
||||
}
|
||||
},
|
||||
"profileSetup": {
|
||||
"cancel": "إلغاء",
|
||||
"descriptionEdit": "حدّث معلومات ملفك الشخصي في المجتمع.",
|
||||
"descriptionFirstTime": "قم بإعداد ملفك الشخصي لإكمال إنشاء ملف المجتمع.",
|
||||
"errors": {
|
||||
"notAuthenticated": "يرجى تسجيل الدخول أولاً قبل المتابعة",
|
||||
"updateFailed": "فشل في تحديث الملف الشخصي، يرجى المحاولة مرة أخرى",
|
||||
"usernameTaken": "معرّف المستخدم هذا مستخدم بالفعل، يرجى اختيار معرّف آخر"
|
||||
},
|
||||
"fields": {
|
||||
"description": {
|
||||
"label": "نبذة شخصية",
|
||||
"maxLength": "النبذة الشخصية يجب ألا تتجاوز 200 حرف",
|
||||
"placeholder": "قدّم نفسك بإيجاز..."
|
||||
},
|
||||
"displayName": {
|
||||
"label": "الاسم الظاهر",
|
||||
"maxLength": "الاسم الظاهر يجب ألا يتجاوز 50 حرفًا",
|
||||
"placeholder": "أدخل اسمك الظاهر",
|
||||
"required": "يرجى إدخال الاسم الظاهر"
|
||||
},
|
||||
"userName": {
|
||||
"label": "معرّف المستخدم",
|
||||
"maxLength": "معرّف المستخدم يجب ألا يتجاوز 32 حرفًا",
|
||||
"minLength": "معرّف المستخدم يجب أن لا يقل عن 3 أحرف",
|
||||
"pattern": "يمكن أن يحتوي معرّف المستخدم على أحرف وأرقام وشرطات سفلية وشرطات فقط",
|
||||
"placeholder": "أدخل معرّف المستخدم الخاص بك",
|
||||
"required": "يرجى إدخال معرّف المستخدم",
|
||||
"tooltip": "معرّف المستخدم هو معرفك الفريد وسيُستخدم في رابط صفحتك الشخصية"
|
||||
}
|
||||
},
|
||||
"getStarted": "ابدأ الآن",
|
||||
"save": "حفظ",
|
||||
"success": "تم تحديث الملف الشخصي بنجاح",
|
||||
"titleEdit": "تعديل الملف الشخصي",
|
||||
"titleFirstTime": "أكمل ملفك الشخصي"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-61
@@ -1,38 +1,4 @@
|
||||
{
|
||||
"context": {
|
||||
"actions": {
|
||||
"delete": "حذف",
|
||||
"edit": "تعديل"
|
||||
},
|
||||
"defaultType": "سياق",
|
||||
"deleteConfirm": "هل أنت متأكد من أنك تريد حذف هذه الذاكرة السياقية؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"deleteTitle": "حذف الذاكرة السياقية",
|
||||
"description": "الوصف",
|
||||
"empty": "لا توجد ذكريات سياقية حالياً",
|
||||
"impact": "درجة التأثير",
|
||||
"source": "المصدر",
|
||||
"urgency": "درجة الإلحاح"
|
||||
},
|
||||
"experience": {
|
||||
"actions": {
|
||||
"delete": "حذف",
|
||||
"edit": "تعديل"
|
||||
},
|
||||
"confidence": "درجة الثقة",
|
||||
"defaultType": "تجربة",
|
||||
"deleteConfirm": "هل أنت متأكد من أنك تريد حذف هذه الذاكرة التجريبية؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"deleteTitle": "حذف الذاكرة التجريبية",
|
||||
"empty": "لا توجد ذكريات تجريبية حالياً",
|
||||
"keyLearning": "التعلم الرئيسي",
|
||||
"situation": "السياق",
|
||||
"source": "المصدر",
|
||||
"steps": {
|
||||
"action": "الإجراء المتخذ",
|
||||
"outcome": "النتيجة المحتملة",
|
||||
"reasoning": "عملية التفكير",
|
||||
"situation": "الخلفية السياقية"
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"empty": "لا توجد ذاكرة هوية حالياً",
|
||||
"filter": {
|
||||
@@ -61,31 +27,5 @@
|
||||
"timeline": "الجدول الزمني"
|
||||
}
|
||||
},
|
||||
"loading": "جارٍ التحميل...",
|
||||
"preference": {
|
||||
"actions": {
|
||||
"delete": "حذف",
|
||||
"edit": "تعديل"
|
||||
},
|
||||
"conclusionDirectives": "توجيهات الاستنتاج",
|
||||
"defaultType": "تفضيل",
|
||||
"deleteConfirm": "هل أنت متأكد من أنك تريد حذف هذه الذاكرة التفضيلية؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"deleteTitle": "حذف الذاكرة التفضيلية",
|
||||
"empty": "لا توجد ذكريات تفضيلية حالياً",
|
||||
"priority": "الأولوية",
|
||||
"source": "المصدر",
|
||||
"suggestions": "الاقتراحات"
|
||||
},
|
||||
"tab": {
|
||||
"contexts": "السياقات",
|
||||
"experiences": "التجارب",
|
||||
"home": "الرئيسية",
|
||||
"identities": "الهويات",
|
||||
"preferences": "التفضيلات",
|
||||
"search": "بحث"
|
||||
},
|
||||
"viewMode": {
|
||||
"masonry": "عرض متدرج",
|
||||
"timeline": "الجدول الزمني"
|
||||
}
|
||||
"loading": "جارٍ التحميل..."
|
||||
}
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
{
|
||||
"builtins": {
|
||||
"lobe-agent-builder": {
|
||||
"apiName": {
|
||||
"getAvailableModels": "الحصول على النماذج المتاحة",
|
||||
"getAvailableTools": "الحصول على الأدوات المتاحة",
|
||||
"getConfig": "الحصول على الإعدادات",
|
||||
"getMeta": "الحصول على البيانات الوصفية",
|
||||
"getPrompt": "الحصول على التعليمات النظامية",
|
||||
"searchMarketTools": "البحث في سوق الإضافات",
|
||||
"searchOfficialTools": "البحث عن الأدوات الرسمية",
|
||||
"setModel": "تعيين النموذج",
|
||||
"setOpeningMessage": "تعيين رسالة البداية",
|
||||
"setOpeningQuestions": "تعيين أسئلة البداية",
|
||||
"togglePlugin": "تبديل الإضافة",
|
||||
"updateChatConfig": "تحديث إعدادات المحادثة",
|
||||
"updateConfig": "تحديث الإعدادات",
|
||||
"updateMeta": "تحديث البيانات الوصفية",
|
||||
"updatePrompt": "تحديث التعليمات النظامية"
|
||||
},
|
||||
"title": "منشئ الوكيل"
|
||||
},
|
||||
"lobe-knowledge-base": {
|
||||
"apiName": {
|
||||
"readKnowledge": "قراءة محتوى قاعدة المعرفة",
|
||||
@@ -44,41 +24,6 @@
|
||||
},
|
||||
"title": "النظام المحلي"
|
||||
},
|
||||
"lobe-page-agent": {
|
||||
"apiName": {
|
||||
"batchUpdate": "تحديث الدُفعة للعُقد",
|
||||
"compareSnapshots": "مقارنة اللقطات",
|
||||
"convertToList": "تحويل إلى قائمة",
|
||||
"createNode": "إنشاء عقدة",
|
||||
"cropImage": "اقتصاص الصورة",
|
||||
"deleteNode": "حذف العقدة",
|
||||
"deleteSnapshot": "حذف اللقطة",
|
||||
"deleteTableColumn": "حذف عمود الجدول",
|
||||
"deleteTableRow": "حذف صف الجدول",
|
||||
"duplicateNode": "نسخ العقدة",
|
||||
"editTitle": "تحرير عنوان المستند",
|
||||
"indentListItem": "زيادة المسافة البادئة لعنصر القائمة",
|
||||
"initPage": "تهيئة المستند",
|
||||
"insertTableColumn": "إدراج عمود في الجدول",
|
||||
"insertTableRow": "إدراج صف في الجدول",
|
||||
"listSnapshots": "عرض اللقطات",
|
||||
"mergeNodes": "دمج العقد",
|
||||
"moveNode": "نقل العقدة",
|
||||
"outdentListItem": "تقليل المسافة البادئة لعنصر القائمة",
|
||||
"replaceText": "استبدال النص",
|
||||
"resizeImage": "تغيير حجم الصورة",
|
||||
"restoreSnapshot": "استعادة اللقطة",
|
||||
"rotateImage": "تدوير الصورة",
|
||||
"saveSnapshot": "حفظ اللقطة",
|
||||
"setImageAlt": "تعيين النص البديل للصورة",
|
||||
"splitNode": "تقسيم العقدة",
|
||||
"toggleListType": "تبديل نوع القائمة",
|
||||
"unwrapNode": "إلغاء تغليف العقدة",
|
||||
"updateNode": "تحديث العقدة",
|
||||
"wrapNodes": "تغليف العقد"
|
||||
},
|
||||
"title": "المستند"
|
||||
},
|
||||
"lobe-web-browsing": {
|
||||
"apiName": {
|
||||
"crawlMultiPages": "قراءة محتوى عدة صفحات",
|
||||
|
||||
+10
-60
@@ -12,6 +12,7 @@
|
||||
"title": "معلومات المساعد"
|
||||
},
|
||||
"chat": {
|
||||
"displayMode": "وضع العرض",
|
||||
"enableHistoryCount": "تمكين عداد الرسائل السابقة",
|
||||
"historyCount": "عدد الرسائل السابقة",
|
||||
"no": "لا",
|
||||
@@ -79,7 +80,6 @@
|
||||
"group": {
|
||||
"aiConfig": "إعدادات الذكاء الاصطناعي",
|
||||
"common": "عام",
|
||||
"market": "السوق",
|
||||
"profile": "الحساب",
|
||||
"system": "النظام"
|
||||
},
|
||||
@@ -223,82 +223,33 @@
|
||||
"messages": {
|
||||
"createVersionFailed": "فشل إنشاء الإصدار: {{message}}",
|
||||
"fetchRemoteFailed": "فشل في جلب بيانات المساعد من السوق",
|
||||
"missingIdentifier": "لا يحتوي هذا المساعد على معرف المجتمع حتى الآن",
|
||||
"missingIdentifier": "المساعد الحالي لا يحتوي على معرف المجتمع",
|
||||
"notAuthenticated": "يرجى تسجيل الدخول إلى حساب المجتمع أولاً",
|
||||
"publishFailed": "فشل النشر: {{message}}"
|
||||
},
|
||||
"submitButton": "نشر",
|
||||
"title": {
|
||||
"submit": "مشاركة في مجتمع المساعدين",
|
||||
"submit": "مشاركة في مجتمع المساعد",
|
||||
"upload": "نشر إصدار جديد"
|
||||
}
|
||||
},
|
||||
"resultModal": {
|
||||
"message": "تم إرسال المساعد الذي أنشأته للمراجعة، وسيتم نشره تلقائيًا بعد الموافقة.",
|
||||
"title": "تم الإرسال بنجاح",
|
||||
"view": "عرض في المجتمع"
|
||||
"view": "الانتقال إلى المجتمع لعرضه"
|
||||
},
|
||||
"submit": {
|
||||
"button": "مشاركة في المجتمع",
|
||||
"tooltip": "شارك المساعد في مجتمع المساعدين"
|
||||
"tooltip": "شارك المساعد في المجتمع"
|
||||
},
|
||||
"upload": {
|
||||
"button": "نشر إصدار جديد",
|
||||
"tooltip": "نشر إصدار جديد في مجتمع المساعدين"
|
||||
"tooltip": "نشر إصدار جديد في مجتمع المساعد"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"success": "تم التحديث بنجاح"
|
||||
},
|
||||
"myAgents": {
|
||||
"actions": {
|
||||
"cancel": "إلغاء",
|
||||
"confirmDeprecate": "تأكيد الإهمال",
|
||||
"deprecate": "إهمال دائم",
|
||||
"deprecateConfirmContent": "بعد الإهمال، سيتم إزالة هذا المساعد نهائيًا من السوق ولن يكون من الممكن إعادة نشره. هذا الإجراء لا يمكن التراجع عنه، يرجى الحذر.",
|
||||
"deprecateConfirmTitle": "هل تريد تأكيد إهمال المساعد؟",
|
||||
"deprecateError": "فشل في إهمال المساعد",
|
||||
"deprecateLoading": "جارٍ إهمال المساعد...",
|
||||
"deprecateSuccess": "تم إهمال المساعد",
|
||||
"edit": "تحرير المساعد",
|
||||
"publish": "نشر المساعد",
|
||||
"publishError": "فشل في نشر المساعد",
|
||||
"publishLoading": "جارٍ نشر المساعد...",
|
||||
"publishSuccess": "تم نشر المساعد",
|
||||
"unpublish": "إلغاء نشر المساعد",
|
||||
"unpublishError": "فشل في إلغاء نشر المساعد",
|
||||
"unpublishLoading": "جارٍ إلغاء نشر المساعد...",
|
||||
"unpublishSuccess": "تم إلغاء نشر المساعد",
|
||||
"viewDetail": "عرض التفاصيل"
|
||||
},
|
||||
"detail": {
|
||||
"category": "الفئة",
|
||||
"description": "الوصف",
|
||||
"identifier": "المعرّف",
|
||||
"title": "تفاصيل المساعد"
|
||||
},
|
||||
"empty": {
|
||||
"description": "لم تقم بنشر أي مساعد في السوق بعد",
|
||||
"title": "لا يوجد مساعدين منشورين"
|
||||
},
|
||||
"errors": {
|
||||
"editFailed": "فشل في تحرير المساعد، يرجى المحاولة لاحقًا",
|
||||
"fetchFailed": "فشل في جلب تفاصيل المساعد",
|
||||
"notAuthenticated": "يرجى تسجيل الدخول إلى حساب السوق أولاً"
|
||||
},
|
||||
"loginRequired": {
|
||||
"button": "تسجيل الدخول إلى حساب السوق",
|
||||
"description": "يرجى تسجيل الدخول إلى حساب السوق لعرض المساعدين الذين قمت بنشرهم",
|
||||
"title": "تسجيل الدخول مطلوب"
|
||||
},
|
||||
"status": {
|
||||
"archived": "مؤرشف",
|
||||
"deprecated": "مهمل",
|
||||
"published": "منشور",
|
||||
"unpublished": "غير منشور"
|
||||
},
|
||||
"title": "مساعدي المنشور"
|
||||
},
|
||||
"plugin": {
|
||||
"addMCPPlugin": "إضافة مكون MCP",
|
||||
"addTooltip": "إضافة البرنامج المساعد",
|
||||
@@ -337,7 +288,7 @@
|
||||
},
|
||||
"submit": "تحديث معلومات المساعد",
|
||||
"tag": {
|
||||
"desc": "سيتم عرض وسم المساعد في مجتمع المساعدين",
|
||||
"desc": "سيتم عرض وسم المساعد في مجتمع المساعد",
|
||||
"placeholder": "الرجاء إدخال العلامة",
|
||||
"title": "العلامة"
|
||||
},
|
||||
@@ -722,7 +673,7 @@
|
||||
"metaMiss": "يرجى استكمال معلومات المساعد قبل التقديم، يجب أن تتضمن الاسم والوصف والعلامة",
|
||||
"placeholder": "الرجاء إدخال معرف المساعد، يجب أن يكون فريدًا، مثل تطوير الويب",
|
||||
"success": "تم إرسال المساعد بنجاح",
|
||||
"tooltips": "مشاركة في مجتمع المساعدين"
|
||||
"tooltips": "مشاركة في مجتمع المساعد"
|
||||
},
|
||||
"submitFooter": {
|
||||
"reset": "إعادة تعيين",
|
||||
@@ -817,13 +768,12 @@
|
||||
"tab": {
|
||||
"about": "حول",
|
||||
"agent": "المساعد الافتراضي",
|
||||
"apikey": "إدارة مفاتيح API",
|
||||
"apikey": "إدارة مفتاح API",
|
||||
"common": "المظهر",
|
||||
"experiment": "تجربة",
|
||||
"hotkey": "اختصارات لوحة المفاتيح",
|
||||
"image": "خدمة الرسم",
|
||||
"llm": "نموذج اللغة",
|
||||
"my-agents": "مساعدي المنشور",
|
||||
"profile": "حسابي",
|
||||
"provider": "مزود خدمة الذكاء الاصطناعي",
|
||||
"proxy": "وكيل الشبكة",
|
||||
@@ -863,7 +813,7 @@
|
||||
"verifyAuth": "لقد أكملت المصادقة"
|
||||
},
|
||||
"notInstalled": "غير مثبت",
|
||||
"notInstalledWarning": "المكون الإضافي غير مثبت حاليًا، وقد يؤثر ذلك على استخدام المساعد",
|
||||
"notInstalledWarning": "المكون الإضافي الحالي غير مثبت، وقد يؤثر ذلك على استخدام المساعد",
|
||||
"plugins": {
|
||||
"enabled": "ممكّنة {{num}}",
|
||||
"groupName": "الإضافات",
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
},
|
||||
"betterAuth": {
|
||||
"errors": {
|
||||
"confirmPasswordRequired": "Моля, потвърдете паролата",
|
||||
"emailExists": "Този имейл вече е регистриран. Моля, влезте директно.",
|
||||
"emailInvalid": "Моля, въведете валиден имейл адрес",
|
||||
"emailNotRegistered": "Този имейл все още не е регистриран",
|
||||
@@ -66,7 +65,6 @@
|
||||
"passwordFormat": "Паролата трябва да съдържа както букви, така и цифри",
|
||||
"passwordMaxLength": "Паролата не може да надвишава 64 знака",
|
||||
"passwordMinLength": "Паролата трябва да бъде поне 8 знака",
|
||||
"passwordMismatch": "Въведените пароли не съвпадат",
|
||||
"passwordRequired": "Моля, въведете парола",
|
||||
"usernameNotRegistered": "Това потребителско име не е регистрирано",
|
||||
"usernameRequired": "Моля, въведете потребителско име"
|
||||
@@ -127,7 +125,6 @@
|
||||
"submit": "Вход"
|
||||
},
|
||||
"signup": {
|
||||
"confirmPasswordPlaceholder": "Моля, потвърдете паролата",
|
||||
"emailPlaceholder": "Моля, въведете имейл адрес",
|
||||
"error": "Регистрацията не бе успешна, моля опитайте отново",
|
||||
"firstNamePlaceholder": "Собствено име",
|
||||
|
||||
@@ -3,22 +3,6 @@
|
||||
"title": "Модел"
|
||||
},
|
||||
"active": "Активен",
|
||||
"agentBuilder": {
|
||||
"installPlugin": {
|
||||
"authRequired": "Облачното MCP разширение изисква удостоверяване",
|
||||
"cancel": "Отказ",
|
||||
"clickApproveToConnect": "Щракнете върху „Одобряване“, за да се свържете и упълномощите тази интеграция",
|
||||
"connectedAndEnabled": "Свързано и активирано",
|
||||
"connectionFailed": "Свързването не бе успешно",
|
||||
"installFailed": "Инсталацията не бе успешна",
|
||||
"installPlugin": "Инсталиране на разширение",
|
||||
"installToEnable": "Инсталирайте това разширение, за да активирате помощника",
|
||||
"installedAndEnabled": "Инсталирано и активирано",
|
||||
"requiresAuth": "Изисква упълномощаване, щракнете върху „Одобряване“, за да се свържете",
|
||||
"retry": "Опитай отново"
|
||||
},
|
||||
"welcome": "Здравей, аз съм **Lobe AI**, твоят експерт по конфигуриране на асистенти. Кажи ми какъв асистент искаш и ще ти помогна да го настроиш."
|
||||
},
|
||||
"agentDefaultMessage": "Здравейте, аз съм **{{name}}**, можете да започнете разговор с мен веднага или да отидете на [Настройки на асистента]({{url}}), за да попълните информацията ми.",
|
||||
"agentDefaultMessageWithSystemRole": "Здравейте, аз съм **{{name}}**. Как мога да ви помогна?",
|
||||
"agentDefaultMessageWithoutEdit": "Здравейте, аз съм **{{name}}**. Как мога да ви помогна?",
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"title": "История на версиите"
|
||||
}
|
||||
},
|
||||
"downloads": "Изтегляния",
|
||||
"list": "Списък с асистенти",
|
||||
"marketSource": {
|
||||
"label": "Превключване на източника на общността",
|
||||
@@ -693,18 +692,6 @@
|
||||
"home": "Начална страница",
|
||||
"model": "Модел",
|
||||
"plugin": "Плъгин",
|
||||
"provider": "Доставчик на модели",
|
||||
"user": "Потребител"
|
||||
},
|
||||
"user": {
|
||||
"agents": "Асистенти",
|
||||
"downloads": "Изтегляния",
|
||||
"editProfile": "Редактиране на профил",
|
||||
"login": "Вход",
|
||||
"logout": "Изход",
|
||||
"myProfile": "Моят профил",
|
||||
"noAgents": "Този потребител все още не е публикувал асистенти",
|
||||
"publishedAgents": "Публикувани асистенти",
|
||||
"website": "Личен уебсайт"
|
||||
"provider": "Доставчик на модели"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,20 +86,6 @@
|
||||
},
|
||||
"newFolder": "Нова папка",
|
||||
"newPage": "Създаване на нов документ",
|
||||
"notion": {
|
||||
"error": "Неуспешен импорт на файл от Notion",
|
||||
"foundFiles": "Намерени {{count}} файла",
|
||||
"importing": "Импортиране на файл от Notion...",
|
||||
"noMarkdownFiles": "Не са намерени Markdown файлове в ZIP архива",
|
||||
"partial": "Успешно импортирани {{success}} файла, неуспешни {{failed}}",
|
||||
"success": "Успешно импортирани {{count}} файла"
|
||||
},
|
||||
"notionGuide": {
|
||||
"cancel": "Отказ от импортиране",
|
||||
"desc": "Моля, първо експортирайте Markdown (ZIP) от Notion. След това щракнете върху „Продължи“ и изберете архивния файл, за да импортирате всички страници.",
|
||||
"ok": "Изберете Notion ZIP",
|
||||
"title": "Импортиране на съдържание от Notion"
|
||||
},
|
||||
"uploadFile": "Качване на файл",
|
||||
"uploadFolder": "Качване на папка"
|
||||
},
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"starter": {
|
||||
"createAgent": "Създаване на асистент",
|
||||
"deepResearch": "Задълбочено проучване",
|
||||
"developing": "В процес на разработка",
|
||||
"image": "Рисуване",
|
||||
"write": "Писане"
|
||||
}
|
||||
}
|
||||
@@ -70,12 +70,12 @@
|
||||
"title": "Бърза смяна на помощника"
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"desc": "Показване или скриване на левия панел",
|
||||
"title": "Показване/Скриване на левия панел"
|
||||
"desc": "Показване или скриване на панела с помощ отляво",
|
||||
"title": "Показване/скриване на панела с помощника"
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"desc": "Показване или скриване на десния панел",
|
||||
"title": "Показване/Скриване на десния панел"
|
||||
"desc": "Показване или скриване на панела с теми отдясно",
|
||||
"title": "Показване/скриване на панела с теми"
|
||||
},
|
||||
"toggleZenMode": {
|
||||
"desc": "В режим на фокус, показвайте само текущия разговор, скривайки другия интерфейс",
|
||||
|
||||
@@ -52,42 +52,5 @@
|
||||
"submit": "Упълномощаването е успешно! Вече можете да публикувате помощник.",
|
||||
"upload": "Упълномощаването е успешно! Вече можете да публикувате нова версия."
|
||||
}
|
||||
},
|
||||
"profileSetup": {
|
||||
"cancel": "Отказ",
|
||||
"descriptionEdit": "Актуализирайте информацията във вашия профил в общността.",
|
||||
"descriptionFirstTime": "Настройте своя профил, за да завършите създаването на профила в общността.",
|
||||
"errors": {
|
||||
"notAuthenticated": "Моля, влезте в системата, преди да продължите",
|
||||
"updateFailed": "Неуспешно обновяване на профила. Моля, опитайте отново",
|
||||
"usernameTaken": "Този потребителски ID вече е зает. Моля, изберете друг"
|
||||
},
|
||||
"fields": {
|
||||
"description": {
|
||||
"label": "Лично представяне",
|
||||
"maxLength": "Личното представяне може да бъде до 200 знака",
|
||||
"placeholder": "Разкажете ни малко за себе си..."
|
||||
},
|
||||
"displayName": {
|
||||
"label": "Псевдоним",
|
||||
"maxLength": "Псевдонимът може да бъде до 50 знака",
|
||||
"placeholder": "Въведете вашия псевдоним",
|
||||
"required": "Моля, въведете псевдоним"
|
||||
},
|
||||
"userName": {
|
||||
"label": "Потребителски ID",
|
||||
"maxLength": "Потребителският ID може да бъде до 32 знака",
|
||||
"minLength": "Потребителският ID трябва да бъде поне 3 знака",
|
||||
"pattern": "Потребителският ID може да съдържа само букви, цифри, долни черти и тирета",
|
||||
"placeholder": "Въведете вашия потребителски ID",
|
||||
"required": "Моля, въведете потребителски ID",
|
||||
"tooltip": "Потребителският ID е вашият уникален идентификатор и ще се използва в линка към вашия профил"
|
||||
}
|
||||
},
|
||||
"getStarted": "Започнете",
|
||||
"save": "Запази",
|
||||
"success": "Профилът е обновен успешно",
|
||||
"titleEdit": "Редактиране на профил",
|
||||
"titleFirstTime": "Завършете своя профил"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,4 @@
|
||||
{
|
||||
"context": {
|
||||
"actions": {
|
||||
"delete": "Изтриване",
|
||||
"edit": "Редактиране"
|
||||
},
|
||||
"defaultType": "Контекст",
|
||||
"deleteConfirm": "Сигурни ли сте, че искате да изтриете тази контекстна памет? Това действие не може да бъде отменено.",
|
||||
"deleteTitle": "Изтриване на контекстна памет",
|
||||
"description": "Описание",
|
||||
"empty": "Няма налична контекстна памет",
|
||||
"impact": "Въздействие",
|
||||
"source": "Източник",
|
||||
"urgency": "Спешност"
|
||||
},
|
||||
"experience": {
|
||||
"actions": {
|
||||
"delete": "Изтриване",
|
||||
"edit": "Редактиране"
|
||||
},
|
||||
"confidence": "Ниво на увереност",
|
||||
"defaultType": "Опит",
|
||||
"deleteConfirm": "Сигурни ли сте, че искате да изтриете тази памет за опит? Това действие не може да бъде отменено.",
|
||||
"deleteTitle": "Изтриване на памет за опит",
|
||||
"empty": "Няма налична памет за опит",
|
||||
"keyLearning": "Ключово знание",
|
||||
"situation": "Контекст",
|
||||
"source": "Източник",
|
||||
"steps": {
|
||||
"action": "Предприето действие",
|
||||
"outcome": "Възможен резултат",
|
||||
"reasoning": "Разсъждение",
|
||||
"situation": "Контекстуален фон"
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"empty": "Няма запаметени самоличности",
|
||||
"filter": {
|
||||
@@ -61,31 +27,5 @@
|
||||
"timeline": "Хронология"
|
||||
}
|
||||
},
|
||||
"loading": "Зареждане...",
|
||||
"preference": {
|
||||
"actions": {
|
||||
"delete": "Изтриване",
|
||||
"edit": "Редактиране"
|
||||
},
|
||||
"conclusionDirectives": "Насоки за заключение",
|
||||
"defaultType": "Предпочитание",
|
||||
"deleteConfirm": "Сигурни ли сте, че искате да изтриете тази памет за предпочитание? Това действие не може да бъде отменено.",
|
||||
"deleteTitle": "Изтриване на памет за предпочитание",
|
||||
"empty": "Няма налична памет за предпочитание",
|
||||
"priority": "Приоритет",
|
||||
"source": "Източник",
|
||||
"suggestions": "Препоръки"
|
||||
},
|
||||
"tab": {
|
||||
"contexts": "Контексти",
|
||||
"experiences": "Опит",
|
||||
"home": "Начало",
|
||||
"identities": "Идентичности",
|
||||
"preferences": "Предпочитания",
|
||||
"search": "Търсене"
|
||||
},
|
||||
"viewMode": {
|
||||
"masonry": "Мозайка",
|
||||
"timeline": "Хронология"
|
||||
}
|
||||
"loading": "Зареждане..."
|
||||
}
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
{
|
||||
"builtins": {
|
||||
"lobe-agent-builder": {
|
||||
"apiName": {
|
||||
"getAvailableModels": "Извличане на налични модели",
|
||||
"getAvailableTools": "Извличане на налични инструменти",
|
||||
"getConfig": "Извличане на конфигурация",
|
||||
"getMeta": "Извличане на метаданни",
|
||||
"getPrompt": "Извличане на системен подкана",
|
||||
"searchMarketTools": "Търсене в магазина за плъгини",
|
||||
"searchOfficialTools": "Търсене на официални инструменти",
|
||||
"setModel": "Задаване на модел",
|
||||
"setOpeningMessage": "Задаване на начално съобщение",
|
||||
"setOpeningQuestions": "Задаване на начални въпроси",
|
||||
"togglePlugin": "Превключване на плъгин",
|
||||
"updateChatConfig": "Актуализиране на конфигурацията на чата",
|
||||
"updateConfig": "Актуализиране на конфигурацията",
|
||||
"updateMeta": "Актуализиране на метаданните",
|
||||
"updatePrompt": "Актуализиране на системния подкана"
|
||||
},
|
||||
"title": "Създател на агент"
|
||||
},
|
||||
"lobe-knowledge-base": {
|
||||
"apiName": {
|
||||
"readKnowledge": "Прочети съдържанието на базата знания",
|
||||
@@ -44,41 +24,6 @@
|
||||
},
|
||||
"title": "Локална система"
|
||||
},
|
||||
"lobe-page-agent": {
|
||||
"apiName": {
|
||||
"batchUpdate": "Партидно актуализиране на възли",
|
||||
"compareSnapshots": "Сравняване на моментни снимки",
|
||||
"convertToList": "Преобразуване в списък",
|
||||
"createNode": "Създаване на възел",
|
||||
"cropImage": "Изрязване на изображение",
|
||||
"deleteNode": "Изтриване на възел",
|
||||
"deleteSnapshot": "Изтриване на моментна снимка",
|
||||
"deleteTableColumn": "Изтриване на колона от таблица",
|
||||
"deleteTableRow": "Изтриване на ред от таблица",
|
||||
"duplicateNode": "Дублиране на възел",
|
||||
"editTitle": "Редактиране на заглавието на документа",
|
||||
"indentListItem": "Увеличаване на отстъпа на елемент от списък",
|
||||
"initPage": "Инициализиране на документа",
|
||||
"insertTableColumn": "Вмъкване на колона в таблица",
|
||||
"insertTableRow": "Вмъкване на ред в таблица",
|
||||
"listSnapshots": "Списък с моментни снимки",
|
||||
"mergeNodes": "Сливане на възли",
|
||||
"moveNode": "Преместване на възел",
|
||||
"outdentListItem": "Намаляване на отстъпа на елемент от списък",
|
||||
"replaceText": "Замяна на текст",
|
||||
"resizeImage": "Преоразмеряване на изображение",
|
||||
"restoreSnapshot": "Възстановяване на моментна снимка",
|
||||
"rotateImage": "Завъртане на изображение",
|
||||
"saveSnapshot": "Запазване на моментна снимка",
|
||||
"setImageAlt": "Задаване на алтернативен текст на изображение",
|
||||
"splitNode": "Разделяне на възел",
|
||||
"toggleListType": "Превключване на тип списък",
|
||||
"unwrapNode": "Разопаковане на възел",
|
||||
"updateNode": "Актуализиране на възел",
|
||||
"wrapNodes": "Опаковане на възли"
|
||||
},
|
||||
"title": "Документ"
|
||||
},
|
||||
"lobe-web-browsing": {
|
||||
"apiName": {
|
||||
"crawlMultiPages": "Прочети съдържание от няколко страници",
|
||||
|
||||
+11
-75
@@ -12,6 +12,7 @@
|
||||
"title": "Информация за асистента"
|
||||
},
|
||||
"chat": {
|
||||
"displayMode": "Режим на показване",
|
||||
"enableHistoryCount": "Разреши броене на историята",
|
||||
"historyCount": "Брой съобщения в историята",
|
||||
"no": "Не",
|
||||
@@ -76,13 +77,6 @@
|
||||
"title": "Нулиране на всички настройки"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"aiConfig": "AI конфигурация",
|
||||
"common": "Общи",
|
||||
"market": "Пазар",
|
||||
"profile": "Акаунт",
|
||||
"system": "Система"
|
||||
},
|
||||
"groupTab": {
|
||||
"chat": "Чат",
|
||||
"members": "Членове",
|
||||
@@ -223,7 +217,7 @@
|
||||
"messages": {
|
||||
"createVersionFailed": "Неуспешно създаване на версия: {{message}}",
|
||||
"fetchRemoteFailed": "Неуспешно извличане на отдалечени данни за асистента",
|
||||
"missingIdentifier": "Този асистент все още няма идентификатор в общността",
|
||||
"missingIdentifier": "Текущият асистент все още няма идентификатор в общността",
|
||||
"notAuthenticated": "Моля, първо влезте в акаунта си в общността",
|
||||
"publishFailed": "Публикуването не бе успешно: {{message}}"
|
||||
},
|
||||
@@ -234,71 +228,22 @@
|
||||
}
|
||||
},
|
||||
"resultModal": {
|
||||
"message": "Вашият асистент е изпратен за преглед. След одобрение ще бъде публикуван автоматично.",
|
||||
"title": "Успешно изпращане",
|
||||
"message": "Вашият асистент е изпратен за преглед. След одобрение ще бъде автоматично публикуван.",
|
||||
"title": "Успешно изпратено",
|
||||
"view": "Виж в общността"
|
||||
},
|
||||
"submit": {
|
||||
"button": "Сподели в общността",
|
||||
"button": "Споделяне в общността",
|
||||
"tooltip": "Споделете асистента в общността"
|
||||
},
|
||||
"upload": {
|
||||
"button": "Публикувай нова версия",
|
||||
"tooltip": "Публикувай нова версия в общността на асистенти"
|
||||
"tooltip": "Публикуване на нова версия в общността на асистенти"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"success": "Актуализацията беше успешна"
|
||||
},
|
||||
"myAgents": {
|
||||
"actions": {
|
||||
"cancel": "Отказ",
|
||||
"confirmDeprecate": "Потвърди оттегляне",
|
||||
"deprecate": "Постоянно оттегляне",
|
||||
"deprecateConfirmContent": "След оттегляне, този агент ще бъде премахнат от пазара завинаги и няма да може да бъде публикуван отново. Това действие е необратимо, моля, действайте внимателно.",
|
||||
"deprecateConfirmTitle": "Сигурни ли сте, че искате да оттеглите агента?",
|
||||
"deprecateError": "Неуспешно оттегляне на агента",
|
||||
"deprecateLoading": "Оттегляне на агента...",
|
||||
"deprecateSuccess": "Агентът е оттеглен",
|
||||
"edit": "Редактиране на агента",
|
||||
"publish": "Публикуване на агента",
|
||||
"publishError": "Неуспешно публикуване на агента",
|
||||
"publishLoading": "Публикуване на агента...",
|
||||
"publishSuccess": "Агентът е публикуван",
|
||||
"unpublish": "Сваляне на агента",
|
||||
"unpublishError": "Неуспешно сваляне на агента",
|
||||
"unpublishLoading": "Сваляне на агента...",
|
||||
"unpublishSuccess": "Агентът е свален",
|
||||
"viewDetail": "Преглед на подробности"
|
||||
},
|
||||
"detail": {
|
||||
"category": "Категория",
|
||||
"description": "Описание",
|
||||
"identifier": "Идентификатор",
|
||||
"title": "Подробности за агента"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Все още не сте публикували агенти на пазара",
|
||||
"title": "Няма публикувани агенти"
|
||||
},
|
||||
"errors": {
|
||||
"editFailed": "Неуспешно редактиране на агента, моля опитайте отново по-късно",
|
||||
"fetchFailed": "Неуспешно зареждане на подробности за агента",
|
||||
"notAuthenticated": "Моля, влезте в акаунта си за пазара"
|
||||
},
|
||||
"loginRequired": {
|
||||
"button": "Вход в акаунта за пазара",
|
||||
"description": "Моля, влезте в акаунта си за пазара, за да видите публикуваните от вас агенти",
|
||||
"title": "Необходим е вход"
|
||||
},
|
||||
"status": {
|
||||
"archived": "Архивиран",
|
||||
"deprecated": "Оттеглен",
|
||||
"published": "Публикуван",
|
||||
"unpublished": "Непубликуван"
|
||||
},
|
||||
"title": "Моите публикувани агенти"
|
||||
},
|
||||
"plugin": {
|
||||
"addMCPPlugin": "Добавяне на MCP плъгин",
|
||||
"addTooltip": "Персонализиран плъгин",
|
||||
@@ -721,8 +666,8 @@
|
||||
"identifier": "Идентификатор на асистента (identifier)",
|
||||
"metaMiss": "Моля, попълнете информацията за агента, преди да го изпратите. Тя трябва да включва име, описание и тагове",
|
||||
"placeholder": "Въведете уникален идентификатор за агента, напр. web-development",
|
||||
"success": "Асистентът е изпратен успешно",
|
||||
"tooltips": "Сподели в общността на асистенти"
|
||||
"success": "Асистентът беше изпратен успешно",
|
||||
"tooltips": "Споделяне в общността на асистенти"
|
||||
},
|
||||
"submitFooter": {
|
||||
"reset": "Нулиране",
|
||||
@@ -817,26 +762,19 @@
|
||||
"tab": {
|
||||
"about": "Относно",
|
||||
"agent": "Агент по подразбиране",
|
||||
"apikey": "Управление на API ключове",
|
||||
"common": "Външен вид",
|
||||
"common": "Общи настройки",
|
||||
"experiment": "Експеримент",
|
||||
"hotkey": "Бързи клавиши",
|
||||
"image": "Услуга за рисуване",
|
||||
"image": "AI рисуване",
|
||||
"llm": "Езиков модел",
|
||||
"my-agents": "Моите публикувани агенти",
|
||||
"profile": "Моят акаунт",
|
||||
"provider": "AI доставчик",
|
||||
"proxy": "Мрежов прокси",
|
||||
"security": "Сигурност",
|
||||
"stats": "Статистика",
|
||||
"storage": "Данни за хранилище",
|
||||
"sync": "Синхронизиране в облака",
|
||||
"system-agent": "Системен асистент",
|
||||
"tts": "Текст към реч",
|
||||
"usage": "Използване"
|
||||
"tts": "Текст към реч"
|
||||
},
|
||||
"tools": {
|
||||
"add": "Интегрирай плъгин",
|
||||
"builtins": {
|
||||
"groupName": "Вградени"
|
||||
},
|
||||
@@ -862,8 +800,6 @@
|
||||
"tools": "инструмента",
|
||||
"verifyAuth": "Удостоверяването е завършено"
|
||||
},
|
||||
"notInstalled": "Не е инсталиран",
|
||||
"notInstalledWarning": "Този плъгин не е инсталиран и може да повлияе на работата на асистента",
|
||||
"plugins": {
|
||||
"enabled": "Активирани: {{num}}",
|
||||
"groupName": "Плъгини",
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
},
|
||||
"betterAuth": {
|
||||
"errors": {
|
||||
"confirmPasswordRequired": "Bitte bestätigen Sie Ihr Passwort",
|
||||
"emailExists": "Diese E-Mail-Adresse ist bereits registriert. Bitte melden Sie sich direkt an.",
|
||||
"emailInvalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||
"emailNotRegistered": "Diese E-Mail-Adresse ist noch nicht registriert",
|
||||
@@ -66,7 +65,6 @@
|
||||
"passwordFormat": "Das Passwort muss Buchstaben und Zahlen enthalten",
|
||||
"passwordMaxLength": "Das Passwort darf maximal 64 Zeichen lang sein",
|
||||
"passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein",
|
||||
"passwordMismatch": "Die beiden eingegebenen Passwörter stimmen nicht überein",
|
||||
"passwordRequired": "Bitte geben Sie ein Passwort ein",
|
||||
"usernameNotRegistered": "Dieser Benutzername ist noch nicht registriert",
|
||||
"usernameRequired": "Bitte geben Sie einen Benutzernamen ein"
|
||||
@@ -127,7 +125,6 @@
|
||||
"submit": "Anmelden"
|
||||
},
|
||||
"signup": {
|
||||
"confirmPasswordPlaceholder": "Bitte bestätigen Sie Ihr Passwort",
|
||||
"emailPlaceholder": "Bitte geben Sie Ihre E-Mail-Adresse ein",
|
||||
"error": "Registrierung fehlgeschlagen, bitte versuchen Sie es erneut",
|
||||
"firstNamePlaceholder": "Vorname",
|
||||
|
||||
@@ -3,22 +3,6 @@
|
||||
"title": "Modell"
|
||||
},
|
||||
"active": "Aktiv",
|
||||
"agentBuilder": {
|
||||
"installPlugin": {
|
||||
"authRequired": "Das Cloud-MCP-Plugin erfordert eine Anmeldung",
|
||||
"cancel": "Abbrechen",
|
||||
"clickApproveToConnect": "Klicken Sie auf „Genehmigen“, um die Verbindung herzustellen und diese Integration zu autorisieren",
|
||||
"connectedAndEnabled": "Verbunden und aktiviert",
|
||||
"connectionFailed": "Verbindung fehlgeschlagen",
|
||||
"installFailed": "Installation fehlgeschlagen",
|
||||
"installPlugin": "Plugin installieren",
|
||||
"installToEnable": "Installieren Sie dieses Plugin, um den Assistenten zu aktivieren",
|
||||
"installedAndEnabled": "Installiert und aktiviert",
|
||||
"requiresAuth": "Autorisierung erforderlich, klicken Sie auf „Genehmigen“, um die Verbindung herzustellen",
|
||||
"retry": "Erneut versuchen"
|
||||
},
|
||||
"welcome": "Hallo, ich bin **Lobe AI**, dein Experte für die Konfiguration von Assistenten. Sag mir, was für einen Assistenten du dir wünschst, und ich helfe dir, ihn einzurichten."
|
||||
},
|
||||
"agentDefaultMessage": "Hallo, ich bin **{{name}}**. Du kannst sofort mit mir sprechen oder zu den [Assistenteneinstellungen]({{url}}) gehen, um meine Informationen zu vervollständigen.",
|
||||
"agentDefaultMessageWithSystemRole": "Hallo, ich bin **{{name}}**. Wie kann ich Ihnen behilflich sein?",
|
||||
"agentDefaultMessageWithoutEdit": "Hallo, ich bin **{{name}}**. Wie kann ich Ihnen behilflich sein?",
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"title": "Versionsverlauf"
|
||||
}
|
||||
},
|
||||
"downloads": "Downloads",
|
||||
"list": "Assistentenliste",
|
||||
"marketSource": {
|
||||
"label": "Community-Quelle wechseln",
|
||||
@@ -693,18 +692,6 @@
|
||||
"home": "Startseite",
|
||||
"model": "Modell",
|
||||
"plugin": "Plugin",
|
||||
"provider": "Modellanbieter",
|
||||
"user": "Benutzer"
|
||||
},
|
||||
"user": {
|
||||
"agents": "Assistenten",
|
||||
"downloads": "Downloads",
|
||||
"editProfile": "Profil bearbeiten",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden",
|
||||
"myProfile": "Mein Profil",
|
||||
"noAgents": "Dieser Benutzer hat noch keine Assistenten veröffentlicht",
|
||||
"publishedAgents": "Veröffentlichte Assistenten",
|
||||
"website": "Persönliche Webseite"
|
||||
"provider": "Modellanbieter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,20 +86,6 @@
|
||||
},
|
||||
"newFolder": "Neuen Ordner erstellen",
|
||||
"newPage": "Neues Dokument",
|
||||
"notion": {
|
||||
"error": "Fehler beim Importieren der Notion-Datei",
|
||||
"foundFiles": "{{count}} Dateien gefunden",
|
||||
"importing": "Notion-Dateien werden importiert...",
|
||||
"noMarkdownFiles": "Keine Markdown-Dateien in der ZIP-Datei gefunden",
|
||||
"partial": "{{success}} Dateien erfolgreich importiert, {{failed}} fehlgeschlagen",
|
||||
"success": "{{count}} Dateien erfolgreich importiert"
|
||||
},
|
||||
"notionGuide": {
|
||||
"cancel": "Jetzt nicht importieren",
|
||||
"desc": "Bitte exportiere zunächst Markdown (ZIP) aus Notion. Klicke dann auf „Weiter“, um das ZIP-Archiv auszuwählen und alle Seiten zu importieren.",
|
||||
"ok": "Notion-ZIP auswählen",
|
||||
"title": "Notion-Inhalte importieren"
|
||||
},
|
||||
"uploadFile": "Datei hochladen",
|
||||
"uploadFolder": "Ordner hochladen"
|
||||
},
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"starter": {
|
||||
"createAgent": "Assistent erstellen",
|
||||
"deepResearch": "Tiefenrecherche",
|
||||
"developing": "In Entwicklung",
|
||||
"image": "Zeichnen",
|
||||
"write": "Schreiben"
|
||||
}
|
||||
}
|
||||
@@ -70,12 +70,12 @@
|
||||
"title": "Schnell zwischen Assistenten wechseln"
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"desc": "Linke Seitenleiste ein- oder ausblenden",
|
||||
"title": "Linke Seitenleiste ein-/ausblenden"
|
||||
"desc": "Linkes Hilfepanel ein- oder ausblenden",
|
||||
"title": "Assistentenpanel ein-/ausblenden"
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"desc": "Rechte Seitenleiste ein- oder ausblenden",
|
||||
"title": "Rechte Seitenleiste ein-/ausblenden"
|
||||
"desc": "Rechtes Themenpanel ein- oder ausblenden",
|
||||
"title": "Themenpanel ein-/ausblenden"
|
||||
},
|
||||
"toggleZenMode": {
|
||||
"desc": "Im Fokusmodus nur die aktuelle Sitzung anzeigen, andere UI ausblenden",
|
||||
|
||||
@@ -52,42 +52,5 @@
|
||||
"submit": "Autorisierung erfolgreich! Du kannst jetzt einen Assistenten veröffentlichen.",
|
||||
"upload": "Autorisierung erfolgreich! Du kannst jetzt eine neue Version veröffentlichen."
|
||||
}
|
||||
},
|
||||
"profileSetup": {
|
||||
"cancel": "Abbrechen",
|
||||
"descriptionEdit": "Aktualisiere deine Community-Profilinformationen.",
|
||||
"descriptionFirstTime": "Richte dein Profil ein, um dein Community-Konto zu vervollständigen.",
|
||||
"errors": {
|
||||
"notAuthenticated": "Bitte melde dich an, um fortzufahren.",
|
||||
"updateFailed": "Profilaktualisierung fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"usernameTaken": "Diese Benutzer-ID ist bereits vergeben. Bitte wähle eine andere."
|
||||
},
|
||||
"fields": {
|
||||
"description": {
|
||||
"label": "Über mich",
|
||||
"maxLength": "Die Beschreibung darf maximal 200 Zeichen enthalten.",
|
||||
"placeholder": "Erzähle etwas über dich..."
|
||||
},
|
||||
"displayName": {
|
||||
"label": "Spitzname",
|
||||
"maxLength": "Der Spitzname darf maximal 50 Zeichen enthalten.",
|
||||
"placeholder": "Gib deinen Spitznamen ein",
|
||||
"required": "Bitte gib einen Spitznamen ein"
|
||||
},
|
||||
"userName": {
|
||||
"label": "Benutzer-ID",
|
||||
"maxLength": "Die Benutzer-ID darf maximal 32 Zeichen enthalten.",
|
||||
"minLength": "Die Benutzer-ID muss mindestens 3 Zeichen lang sein.",
|
||||
"pattern": "Die Benutzer-ID darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.",
|
||||
"placeholder": "Gib deine Benutzer-ID ein",
|
||||
"required": "Bitte gib eine Benutzer-ID ein",
|
||||
"tooltip": "Die Benutzer-ID ist dein eindeutiger Identifikator und wird in deinem Profil-Link verwendet."
|
||||
}
|
||||
},
|
||||
"getStarted": "Loslegen",
|
||||
"save": "Speichern",
|
||||
"success": "Profil erfolgreich aktualisiert",
|
||||
"titleEdit": "Profil bearbeiten",
|
||||
"titleFirstTime": "Vervollständige dein Profil"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,4 @@
|
||||
{
|
||||
"context": {
|
||||
"actions": {
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten"
|
||||
},
|
||||
"defaultType": "Kontext",
|
||||
"deleteConfirm": "Möchten Sie diesen Kontextspeicher wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"deleteTitle": "Kontextspeicher löschen",
|
||||
"description": "Beschreibung",
|
||||
"empty": "Keine Kontextspeicher vorhanden",
|
||||
"impact": "Auswirkungsgrad",
|
||||
"source": "Quelle",
|
||||
"urgency": "Dringlichkeit"
|
||||
},
|
||||
"experience": {
|
||||
"actions": {
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten"
|
||||
},
|
||||
"confidence": "Vertrauensgrad",
|
||||
"defaultType": "Erfahrung",
|
||||
"deleteConfirm": "Möchten Sie diesen Erfahrungsspeicher wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"deleteTitle": "Erfahrungsspeicher löschen",
|
||||
"empty": "Keine Erfahrungsspeicher vorhanden",
|
||||
"keyLearning": "Zentrale Erkenntnis",
|
||||
"situation": "Kontext",
|
||||
"source": "Quelle",
|
||||
"steps": {
|
||||
"action": "Maßnahme",
|
||||
"outcome": "Mögliches Ergebnis",
|
||||
"reasoning": "Begründung",
|
||||
"situation": "Situationshintergrund"
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"empty": "Keine Identitätserinnerung vorhanden",
|
||||
"filter": {
|
||||
@@ -61,31 +27,5 @@
|
||||
"timeline": "Zeitleiste"
|
||||
}
|
||||
},
|
||||
"loading": "Wird geladen...",
|
||||
"preference": {
|
||||
"actions": {
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten"
|
||||
},
|
||||
"conclusionDirectives": "Schlussfolgerungsanweisungen",
|
||||
"defaultType": "Präferenz",
|
||||
"deleteConfirm": "Möchten Sie diesen Präferenzspeicher wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"deleteTitle": "Präferenzspeicher löschen",
|
||||
"empty": "Keine Präferenzspeicher vorhanden",
|
||||
"priority": "Priorität",
|
||||
"source": "Quelle",
|
||||
"suggestions": "Vorschläge"
|
||||
},
|
||||
"tab": {
|
||||
"contexts": "Kontexte",
|
||||
"experiences": "Erfahrungen",
|
||||
"home": "Startseite",
|
||||
"identities": "Identitäten",
|
||||
"preferences": "Präferenzen",
|
||||
"search": "Suche"
|
||||
},
|
||||
"viewMode": {
|
||||
"masonry": "Kachelansicht",
|
||||
"timeline": "Zeitstrahl"
|
||||
}
|
||||
"loading": "Wird geladen..."
|
||||
}
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
{
|
||||
"builtins": {
|
||||
"lobe-agent-builder": {
|
||||
"apiName": {
|
||||
"getAvailableModels": "Verfügbare Modelle abrufen",
|
||||
"getAvailableTools": "Verfügbare Werkzeuge abrufen",
|
||||
"getConfig": "Konfiguration abrufen",
|
||||
"getMeta": "Metadaten abrufen",
|
||||
"getPrompt": "System-Prompt abrufen",
|
||||
"searchMarketTools": "Plugin-Marktplatz durchsuchen",
|
||||
"searchOfficialTools": "Offizielle Werkzeuge durchsuchen",
|
||||
"setModel": "Modell festlegen",
|
||||
"setOpeningMessage": "Begrüßungsnachricht festlegen",
|
||||
"setOpeningQuestions": "Einstiegsfragen festlegen",
|
||||
"togglePlugin": "Plugin umschalten",
|
||||
"updateChatConfig": "Chat-Konfiguration aktualisieren",
|
||||
"updateConfig": "Konfiguration aktualisieren",
|
||||
"updateMeta": "Metadaten aktualisieren",
|
||||
"updatePrompt": "System-Prompt aktualisieren"
|
||||
},
|
||||
"title": "Agenten-Builder"
|
||||
},
|
||||
"lobe-knowledge-base": {
|
||||
"apiName": {
|
||||
"readKnowledge": "Wissensdatenbank lesen",
|
||||
@@ -44,41 +24,6 @@
|
||||
},
|
||||
"title": "Lokales System"
|
||||
},
|
||||
"lobe-page-agent": {
|
||||
"apiName": {
|
||||
"batchUpdate": "Knoten stapelweise aktualisieren",
|
||||
"compareSnapshots": "Schnappschüsse vergleichen",
|
||||
"convertToList": "In Liste umwandeln",
|
||||
"createNode": "Knoten erstellen",
|
||||
"cropImage": "Bild zuschneiden",
|
||||
"deleteNode": "Knoten löschen",
|
||||
"deleteSnapshot": "Schnappschuss löschen",
|
||||
"deleteTableColumn": "Tabellenspalte löschen",
|
||||
"deleteTableRow": "Tabellenzeile löschen",
|
||||
"duplicateNode": "Knoten duplizieren",
|
||||
"editTitle": "Dokumenttitel bearbeiten",
|
||||
"indentListItem": "Listenelement einrücken",
|
||||
"initPage": "Dokument initialisieren",
|
||||
"insertTableColumn": "Tabellenspalte einfügen",
|
||||
"insertTableRow": "Tabellenzeile einfügen",
|
||||
"listSnapshots": "Schnappschüsse auflisten",
|
||||
"mergeNodes": "Knoten zusammenführen",
|
||||
"moveNode": "Knoten verschieben",
|
||||
"outdentListItem": "Einrückung des Listenelements entfernen",
|
||||
"replaceText": "Text ersetzen",
|
||||
"resizeImage": "Bildgröße ändern",
|
||||
"restoreSnapshot": "Schnappschuss wiederherstellen",
|
||||
"rotateImage": "Bild drehen",
|
||||
"saveSnapshot": "Schnappschuss speichern",
|
||||
"setImageAlt": "Alternativtext für Bild festlegen",
|
||||
"splitNode": "Knoten aufteilen",
|
||||
"toggleListType": "Listentyp umschalten",
|
||||
"unwrapNode": "Knoten entpacken",
|
||||
"updateNode": "Knoten aktualisieren",
|
||||
"wrapNodes": "Knoten umschließen"
|
||||
},
|
||||
"title": "Dokument"
|
||||
},
|
||||
"lobe-web-browsing": {
|
||||
"apiName": {
|
||||
"crawlMultiPages": "Inhalte mehrerer Seiten lesen",
|
||||
|
||||
+14
-81
@@ -2,7 +2,6 @@
|
||||
"about": {
|
||||
"title": "Über"
|
||||
},
|
||||
"advancedSettings": "Erweiterte Einstellungen",
|
||||
"agentInfoDescription": {
|
||||
"basic": {
|
||||
"avatar": "Profilbild",
|
||||
@@ -12,6 +11,7 @@
|
||||
"title": "Assistenteninformationen"
|
||||
},
|
||||
"chat": {
|
||||
"displayMode": "Anzeigemodus",
|
||||
"enableHistoryCount": "Anzahl vergangener Nachrichten aktivieren",
|
||||
"historyCount": "Anzahl vergangener Nachrichten",
|
||||
"no": "Nein",
|
||||
@@ -76,13 +76,6 @@
|
||||
"title": "Alle Einstellungen zurücksetzen"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"aiConfig": "KI-Konfiguration",
|
||||
"common": "Allgemein",
|
||||
"market": "Marktplatz",
|
||||
"profile": "Konto",
|
||||
"system": "System"
|
||||
},
|
||||
"groupTab": {
|
||||
"chat": "Chat",
|
||||
"members": "Mitglieder",
|
||||
@@ -223,82 +216,32 @@
|
||||
"messages": {
|
||||
"createVersionFailed": "Versionserstellung fehlgeschlagen: {{message}}",
|
||||
"fetchRemoteFailed": "Fehler beim Abrufen der entfernten Assistentendaten",
|
||||
"missingIdentifier": "Dieser Assistent hat noch keine Community-Kennung",
|
||||
"notAuthenticated": "Bitte melden Sie sich zuerst mit Ihrem Community-Konto an",
|
||||
"missingIdentifier": "Der aktuelle Assistent hat noch keinen Markt-Bezeichner",
|
||||
"notAuthenticated": "Bitte melden Sie sich zuerst mit Ihrem Marktkonto an",
|
||||
"publishFailed": "Veröffentlichung fehlgeschlagen: {{message}}"
|
||||
},
|
||||
"submitButton": "Veröffentlichen",
|
||||
"title": {
|
||||
"submit": "In der Assistenten-Community teilen",
|
||||
"submit": "Im Assistenten-Markt teilen",
|
||||
"upload": "Neue Version veröffentlichen"
|
||||
}
|
||||
},
|
||||
"resultModal": {
|
||||
"message": "Ihr erstellter Assistent wurde zur Überprüfung eingereicht und wird nach erfolgreicher Prüfung automatisch veröffentlicht.",
|
||||
"title": "Erfolgreich eingereicht",
|
||||
"view": "In der Community ansehen"
|
||||
"message": "Der Assistent wurde zur Überprüfung eingereicht. Nach erfolgreicher Prüfung wird er automatisch veröffentlicht. Klicken Sie auf „Im Markt ansehen“, um den veröffentlichten Assistenten zu sehen.",
|
||||
"view": "Im Markt ansehen"
|
||||
},
|
||||
"submit": {
|
||||
"button": "In der Community teilen",
|
||||
"tooltip": "Assistent in der Community veröffentlichen"
|
||||
"button": "Im Markt teilen",
|
||||
"tooltip": "Assistent im Markt veröffentlichen"
|
||||
},
|
||||
"upload": {
|
||||
"button": "Neue Version veröffentlichen",
|
||||
"tooltip": "Neue Version in der Assistenten-Community veröffentlichen"
|
||||
"tooltip": "Neue Version im Assistenten-Markt veröffentlichen"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"success": "Erfolgreich aktualisiert"
|
||||
},
|
||||
"myAgents": {
|
||||
"actions": {
|
||||
"cancel": "Abbrechen",
|
||||
"confirmDeprecate": "Veraltete Version bestätigen",
|
||||
"deprecate": "Dauerhaft entfernen",
|
||||
"deprecateConfirmContent": "Nach dem Entfernen wird dieser Assistent dauerhaft aus dem Marktplatz entfernt und kann nicht erneut veröffentlicht werden. Dieser Vorgang ist unwiderruflich. Bitte seien Sie vorsichtig.",
|
||||
"deprecateConfirmTitle": "Assistent wirklich entfernen?",
|
||||
"deprecateError": "Assistent konnte nicht entfernt werden",
|
||||
"deprecateLoading": "Assistent wird entfernt...",
|
||||
"deprecateSuccess": "Assistent wurde entfernt",
|
||||
"edit": "Assistent bearbeiten",
|
||||
"publish": "Assistent veröffentlichen",
|
||||
"publishError": "Veröffentlichung des Assistenten fehlgeschlagen",
|
||||
"publishLoading": "Assistent wird veröffentlicht...",
|
||||
"publishSuccess": "Assistent wurde veröffentlicht",
|
||||
"unpublish": "Assistent zurückziehen",
|
||||
"unpublishError": "Zurückziehen des Assistenten fehlgeschlagen",
|
||||
"unpublishLoading": "Assistent wird zurückgezogen...",
|
||||
"unpublishSuccess": "Assistent wurde zurückgezogen",
|
||||
"viewDetail": "Details anzeigen"
|
||||
},
|
||||
"detail": {
|
||||
"category": "Kategorie",
|
||||
"description": "Beschreibung",
|
||||
"identifier": "Bezeichner",
|
||||
"title": "Assistenten-Details"
|
||||
},
|
||||
"empty": {
|
||||
"description": "Du hast noch keinen Assistenten im Marktplatz veröffentlicht",
|
||||
"title": "Keine veröffentlichten Assistenten"
|
||||
},
|
||||
"errors": {
|
||||
"editFailed": "Bearbeiten des Assistenten fehlgeschlagen. Bitte versuche es später erneut.",
|
||||
"fetchFailed": "Details des Assistenten konnten nicht geladen werden",
|
||||
"notAuthenticated": "Bitte melde dich zuerst mit deinem Marktplatz-Konto an"
|
||||
},
|
||||
"loginRequired": {
|
||||
"button": "Im Marktplatz anmelden",
|
||||
"description": "Bitte melde dich mit deinem Marktplatz-Konto an, um deine veröffentlichten Assistenten zu sehen",
|
||||
"title": "Anmeldung erforderlich"
|
||||
},
|
||||
"status": {
|
||||
"archived": "Archiviert",
|
||||
"deprecated": "Veraltet",
|
||||
"published": "Veröffentlicht",
|
||||
"unpublished": "Nicht veröffentlicht"
|
||||
},
|
||||
"title": "Meine veröffentlichten Assistenten"
|
||||
},
|
||||
"plugin": {
|
||||
"addMCPPlugin": "MCP-Plugin hinzufügen",
|
||||
"addTooltip": "Benutzerdefiniertes Plugin",
|
||||
@@ -337,7 +280,7 @@
|
||||
},
|
||||
"submit": "Assistenteninformationen aktualisieren",
|
||||
"tag": {
|
||||
"desc": "Die Tags des Assistenten werden in der Assistenten-Community angezeigt",
|
||||
"desc": "Die Assistenten-Tags werden im Assistentenmarkt angezeigt",
|
||||
"placeholder": "Bitte geben Sie ein Tag ein",
|
||||
"title": "Tag"
|
||||
},
|
||||
@@ -721,8 +664,7 @@
|
||||
"identifier": "Assistenten-Bezeichner (identifier)",
|
||||
"metaMiss": "Bitte vervollständigen Sie die Assistenteninformationen, einschließlich Name, Beschreibung und Tags, bevor Sie sie einreichen.",
|
||||
"placeholder": "Geben Sie die Kennung des Assistenten ein, die eindeutig sein muss, z. B. Web-Entwicklung",
|
||||
"success": "Assistent erfolgreich eingereicht",
|
||||
"tooltips": "In der Assistenten-Community teilen"
|
||||
"tooltips": "Auf dem Assistentenmarkt teilen"
|
||||
},
|
||||
"submitFooter": {
|
||||
"reset": "Zurücksetzen",
|
||||
@@ -817,26 +759,19 @@
|
||||
"tab": {
|
||||
"about": "Über",
|
||||
"agent": "Standard-Assistent",
|
||||
"apikey": "API-Schlüsselverwaltung",
|
||||
"common": "Erscheinungsbild",
|
||||
"common": "Allgemeine Einstellungen",
|
||||
"experiment": "Experiment",
|
||||
"hotkey": "Tastenkombinationen",
|
||||
"image": "Bildgenerierungsdienst",
|
||||
"image": "AI-Zeichnung",
|
||||
"llm": "Sprachmodell",
|
||||
"my-agents": "Meine veröffentlichten Assistenten",
|
||||
"profile": "Mein Konto",
|
||||
"provider": "KI-Dienstanbieter",
|
||||
"proxy": "Netzwerkproxy",
|
||||
"security": "Sicherheit",
|
||||
"stats": "Statistiken",
|
||||
"storage": "Datenspeicher",
|
||||
"sync": "Cloud-Synchronisierung",
|
||||
"system-agent": "Systemassistent",
|
||||
"tts": "Sprachdienste",
|
||||
"usage": "Nutzungsstatistik"
|
||||
"tts": "Sprachdienste"
|
||||
},
|
||||
"tools": {
|
||||
"add": "Plugin integrieren",
|
||||
"builtins": {
|
||||
"groupName": "Integriert"
|
||||
},
|
||||
@@ -862,8 +797,6 @@
|
||||
"tools": "Tools",
|
||||
"verifyAuth": "Ich habe die Authentifizierung abgeschlossen"
|
||||
},
|
||||
"notInstalled": "Nicht installiert",
|
||||
"notInstalledWarning": "Dieses Plugin ist derzeit nicht installiert und könnte die Nutzung des Assistenten beeinträchtigen",
|
||||
"plugins": {
|
||||
"enabled": "Aktiviert: {{num}}",
|
||||
"groupName": "Plugins",
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
},
|
||||
"betterAuth": {
|
||||
"errors": {
|
||||
"confirmPasswordRequired": "Please confirm your password",
|
||||
"emailExists": "This email is already registered. Please sign in instead",
|
||||
"emailInvalid": "Please enter a valid email address or username",
|
||||
"emailNotRegistered": "This email or username is not registered",
|
||||
@@ -66,7 +65,6 @@
|
||||
"passwordFormat": "Password must contain both letters and numbers",
|
||||
"passwordMaxLength": "Password must not exceed 64 characters",
|
||||
"passwordMinLength": "Password must be at least 8 characters",
|
||||
"passwordMismatch": "The passwords do not match",
|
||||
"passwordRequired": "Please enter your password",
|
||||
"usernameNotRegistered": "This username is not registered",
|
||||
"usernameRequired": "Please enter your username"
|
||||
@@ -127,7 +125,6 @@
|
||||
"submit": "Sign In"
|
||||
},
|
||||
"signup": {
|
||||
"confirmPasswordPlaceholder": "Confirm your password",
|
||||
"emailPlaceholder": "Enter your email address",
|
||||
"error": "Sign up failed, please try again",
|
||||
"firstNamePlaceholder": "First Name",
|
||||
|
||||
@@ -3,22 +3,6 @@
|
||||
"title": "Model"
|
||||
},
|
||||
"active": "Active",
|
||||
"agentBuilder": {
|
||||
"installPlugin": {
|
||||
"authRequired": "Authentication required for cloud MCP plugins",
|
||||
"cancel": "Cancel",
|
||||
"clickApproveToConnect": "Click \"Approve\" to connect and authorize this integration",
|
||||
"connectedAndEnabled": "Connected and enabled",
|
||||
"connectionFailed": "Connection failed",
|
||||
"installFailed": "Installation failed",
|
||||
"installPlugin": "Install Plugin",
|
||||
"installToEnable": "Install this plugin to enable it for the agent",
|
||||
"installedAndEnabled": "Installed and enabled",
|
||||
"requiresAuth": "Requires authorization. Click \"Approve\" to connect",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"welcome": "Hello, I'm **Lobe AI**, your assistant configuration expert. Tell me what kind of assistant you want, and I'll set it up for you."
|
||||
},
|
||||
"agentDefaultMessage": "Hello, I am **{{name}}**. You can start a conversation with me right away, or you can go to [Assistant Settings]({{url}}) to complete my information.",
|
||||
"agentDefaultMessageWithSystemRole": "Hello, I am **{{name}}**. How can I assist you today?",
|
||||
"agentDefaultMessageWithoutEdit": "Hello, I am **{{name}}**. How can I assist you today?",
|
||||
@@ -106,13 +90,8 @@
|
||||
},
|
||||
"groupDescription": "Team Description",
|
||||
"groupSidebar": {
|
||||
"agentProfile": {
|
||||
"chat": "Chat",
|
||||
"model": "Model"
|
||||
},
|
||||
"members": {
|
||||
"addMember": "Add Member",
|
||||
"enableOrchestrator": "Enable Host",
|
||||
"memberSettings": "Member Settings",
|
||||
"orchestrator": "Host",
|
||||
"orchestratorThinking": "The host is thinking...",
|
||||
@@ -385,7 +364,6 @@
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"groupProfile": "Group Profile",
|
||||
"profile": "Assistant Profile",
|
||||
"search": "Search"
|
||||
},
|
||||
@@ -453,7 +431,6 @@
|
||||
"clear": "Clear Speech"
|
||||
},
|
||||
"untitledAgent": "Untitled Assistant",
|
||||
"untitledGroup": "Untitled Group",
|
||||
"updateAgent": "Update Assistant Information",
|
||||
"upload": {
|
||||
"action": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user