mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +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 |
@@ -0,0 +1,161 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
---
|
||||
# 如何添加新的快捷键:开发者指南
|
||||
|
||||
本指南将带您一步步地向 LobeChat 添加一个新的快捷键功能。我们将通过一个完整示例,演示从定义到实现的整个过程。
|
||||
|
||||
## 示例场景
|
||||
|
||||
假设我们要添加一个新的快捷键功能:**快速清空聊天记录**,快捷键为 `Mod+Shift+Backspace`。
|
||||
|
||||
## 步骤 1:更新快捷键常量定义
|
||||
|
||||
首先,在 `src/types/hotkey.ts` 中更新 `HotkeyEnum`:
|
||||
|
||||
```typescript
|
||||
export const HotkeyEnum = {
|
||||
// 已有的快捷键...
|
||||
AddUserMessage: 'addUserMessage',
|
||||
EditMessage: 'editMessage',
|
||||
|
||||
// 新增快捷键
|
||||
ClearChat: 'clearChat', // 添加这一行
|
||||
|
||||
// 其他已有快捷键...
|
||||
} as const;
|
||||
```
|
||||
|
||||
## 步骤 2:注册默认快捷键
|
||||
|
||||
在 `src/const/hotkeys.ts` 中添加快捷键的默认配置:
|
||||
|
||||
```typescript
|
||||
import { KeyMapEnum as Key, combineKeys } from '@lobehub/ui';
|
||||
|
||||
// ...现有代码
|
||||
|
||||
export const HOTKEYS_REGISTRATION: HotkeyRegistration = [
|
||||
// 现有的快捷键配置...
|
||||
|
||||
// 添加新的快捷键配置
|
||||
{
|
||||
group: HotkeyGroupEnum.Conversation, // 归类到会话操作组
|
||||
id: HotkeyEnum.ClearChat,
|
||||
keys: combineKeys([Key.Mod, Key.Shift, Key.Backspace]),
|
||||
scopes: [HotkeyScopeEnum.Chat], // 在聊天作用域下生效
|
||||
},
|
||||
|
||||
// 其他现有快捷键...
|
||||
];
|
||||
```
|
||||
|
||||
## 步骤 3:添加国际化翻译
|
||||
|
||||
在 `src/locales/default/hotkey.ts` 中添加对应的文本描述:
|
||||
|
||||
```typescript
|
||||
import { HotkeyI18nTranslations } from '@/types/hotkey';
|
||||
|
||||
const hotkey: HotkeyI18nTranslations = {
|
||||
// 现有翻译...
|
||||
|
||||
// 添加新快捷键的翻译
|
||||
clearChat: {
|
||||
desc: '清空当前会话的所有消息记录',
|
||||
title: '清空聊天记录',
|
||||
},
|
||||
|
||||
// 其他现有翻译...
|
||||
};
|
||||
|
||||
export default hotkey;
|
||||
```
|
||||
|
||||
如需支持其他语言,还需要在相应的语言文件中添加对应翻译。
|
||||
|
||||
## 步骤 4:创建并注册快捷键 Hook
|
||||
|
||||
在 `src/hooks/useHotkeys/chatScope.ts` 中添加新的 Hook:
|
||||
|
||||
```typescript
|
||||
export const useClearChatHotkey = () => {
|
||||
const clearMessages = useChatStore((s) => s.clearMessages);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useHotkeyById(HotkeyEnum.ClearChat, showConfirm);
|
||||
};
|
||||
|
||||
// 注册聚合
|
||||
|
||||
export const useRegisterChatHotkeys = () => {
|
||||
const { enableScope, disableScope } = useHotkeysContext();
|
||||
|
||||
useOpenChatSettingsHotkey();
|
||||
// ...其他快捷键
|
||||
useClearChatHotkey();
|
||||
|
||||
useEffect(() => {
|
||||
enableScope(HotkeyScopeEnum.Chat);
|
||||
return () => disableScope(HotkeyScopeEnum.Chat);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
```
|
||||
|
||||
## 步骤 5:给相应 UI 元素添加 Tooltip 提示(可选)
|
||||
|
||||
如果有对应的 UI 按钮,可以添加快捷键提示:
|
||||
|
||||
```tsx
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from '@lobehub/ui';
|
||||
import { Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { HotkeyEnum } from '@/types/hotkey';
|
||||
|
||||
const ClearChatButton = () => {
|
||||
const { t } = useTranslation(['hotkey', 'chat']);
|
||||
const clearChatHotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ClearChat));
|
||||
|
||||
// 获取清空聊天的方法
|
||||
const clearMessages = useChatStore((s) => s.clearMessages);
|
||||
|
||||
return (
|
||||
<Tooltip hotkey={clearChatHotkey} title={t('clearChat.title', { ns: 'hotkey' })}>
|
||||
<Button icon={<DeleteOutlined />} onClick={clearMessages} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 步骤 6:测试新快捷键
|
||||
|
||||
1. 启动开发服务器
|
||||
2. 打开聊天页面
|
||||
3. 按下设置的快捷键组合(`Cmd+Shift+Backspace` 或 `Ctrl+Shift+Backspace`)
|
||||
4. 确认功能正常工作
|
||||
5. 检查快捷键设置面板中是否正确显示了新快捷键
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **作用域考虑**:根据功能决定快捷键应属于全局作用域还是聊天作用域
|
||||
2. **分组合理**:将快捷键放在合适的功能组中(System/Layout/Conversation)
|
||||
3. **冲突检查**:确保新快捷键不会与现有系统、浏览器或应用快捷键冲突
|
||||
4. **平台适配**:使用 `Key.Mod` 而非硬编码 `Ctrl` 或 `Cmd`,以适配不同平台
|
||||
5. **提供清晰描述**:为快捷键添加明确的标题和描述,帮助用户理解功能
|
||||
|
||||
按照以上步骤,您可以轻松地向系统添加新的快捷键功能,提升用户体验。如有特殊需求,如桌面专属快捷键,可以通过 `isDesktop` 标记进行区分处理。
|
||||
|
||||
## 常见问题排查
|
||||
|
||||
- **快捷键未生效**:检查作用域是否正确,以及是否在 RegisterHotkeys 中调用了对应的 hook
|
||||
- **快捷键设置面板未显示**:确认在 HOTKEYS_REGISTRATION 中正确配置了快捷键
|
||||
- **快捷键冲突**:在 HotkeyInput 组件中可以检测到冲突,用户会看到警告
|
||||
- **功能在某些页面失效**:确认是否注册在了正确的作用域,以及相关页面是否激活了该作用域
|
||||
|
||||
通过这些步骤,您可以确保新添加的快捷键功能稳定、可靠且用户友好。
|
||||
@@ -0,0 +1,138 @@
|
||||
# Recent Data 使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
Recent 数据(recentTopics, recentResources, recentPages)存储在 session store 中,可以在应用的任何地方访问。
|
||||
|
||||
## 数据初始化
|
||||
|
||||
在应用顶层(如 `RecentHydration.tsx`)中初始化所有 recent 数据:
|
||||
|
||||
```tsx
|
||||
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
|
||||
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
|
||||
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
|
||||
|
||||
const App = () => {
|
||||
// 初始化所有 recent 数据
|
||||
useInitRecentTopic();
|
||||
useInitRecentResource();
|
||||
useInitRecentPage();
|
||||
|
||||
return <YourComponents />;
|
||||
};
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:直接从 Store 读取(推荐用于多处使用)
|
||||
|
||||
在任何组件中直接访问 store 中的数据:
|
||||
|
||||
```tsx
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { recentSelectors } from '@/store/session/selectors';
|
||||
|
||||
const Component = () => {
|
||||
// 读取数据
|
||||
const recentTopics = useSessionStore(recentSelectors.recentTopics);
|
||||
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
|
||||
|
||||
if (!isInit) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{recentTopics.map(topic => (
|
||||
<div key={topic.id}>{topic.title}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 方式二:使用 Hook 返回的数据(用于单一组件)
|
||||
|
||||
```tsx
|
||||
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
|
||||
|
||||
const Component = () => {
|
||||
const { data: recentTopics, isLoading } = useInitRecentTopic();
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
return <div>{/* 使用 recentTopics */}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## 可用的 Selectors
|
||||
|
||||
### Recent Topics (最近话题)
|
||||
|
||||
```tsx
|
||||
import { recentSelectors } from '@/store/session/selectors';
|
||||
|
||||
// 数据
|
||||
const recentTopics = useSessionStore(recentSelectors.recentTopics);
|
||||
// 类型: RecentTopic[]
|
||||
|
||||
// 初始化状态
|
||||
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
|
||||
// 类型: boolean
|
||||
```
|
||||
|
||||
**RecentTopic 类型:**
|
||||
```typescript
|
||||
interface RecentTopic {
|
||||
agent: {
|
||||
avatar: string | null;
|
||||
backgroundColor: string | null;
|
||||
id: string;
|
||||
title: string | null;
|
||||
} | null;
|
||||
id: string;
|
||||
title: string | null;
|
||||
updatedAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### Recent Resources (最近文件)
|
||||
|
||||
```tsx
|
||||
import { recentSelectors } from '@/store/session/selectors';
|
||||
|
||||
// 数据
|
||||
const recentResources = useSessionStore(recentSelectors.recentResources);
|
||||
// 类型: FileListItem[]
|
||||
|
||||
// 初始化状态
|
||||
const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
|
||||
// 类型: boolean
|
||||
```
|
||||
|
||||
### Recent Pages (最近页面)
|
||||
|
||||
```tsx
|
||||
import { recentSelectors } from '@/store/session/selectors';
|
||||
|
||||
// 数据
|
||||
const recentPages = useSessionStore(recentSelectors.recentPages);
|
||||
// 类型: any[]
|
||||
|
||||
// 初始化状态
|
||||
const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
|
||||
// 类型: boolean
|
||||
```
|
||||
|
||||
## 特性
|
||||
|
||||
1. **自动登录检测**:只有在用户登录时才会加载数据
|
||||
2. **数据缓存**:数据存储在 store 中,多处使用无需重复加载
|
||||
3. **自动刷新**:使用 SWR,在用户重新聚焦时自动刷新(5分钟间隔)
|
||||
4. **类型安全**:完整的 TypeScript 类型定义
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **初始化位置**:在应用顶层统一初始化所有 recent 数据
|
||||
2. **数据访问**:使用 selectors 从 store 读取数据
|
||||
3. **多处使用**:同一数据在多个组件中使用时,推荐使用方式一(直接从 store 读取)
|
||||
4. **性能优化**:使用 selector 确保只有相关数据变化时才重新渲染
|
||||
+1
-1
@@ -24,7 +24,7 @@ Desktop.ini
|
||||
.windsurfrules
|
||||
*.code-workspace
|
||||
.vscode/sessions.json
|
||||
|
||||
prd
|
||||
# Temporary files
|
||||
.temp/
|
||||
temp/
|
||||
|
||||
+3
-1
@@ -30,7 +30,9 @@ module.exports = defineConfig({
|
||||
jsonMode: true,
|
||||
},
|
||||
markdown: {
|
||||
reference: '你需要保持 mdx 的组件格式,输出文本不需要在最外层包裹任何代码块语法',
|
||||
reference:
|
||||
'你需要保持 mdx 的组件格式,输出文本不需要在最外层包裹任何代码块语法。以下是一些词汇的固定翻译:\n' +
|
||||
JSON.stringify(require('./glossary.json'), null, 2),
|
||||
entry: ['./README.zh-CN.md', './contributing/**/*.zh-CN.md', './docs/**/*.zh-CN.mdx'],
|
||||
entryLocale: 'zh-CN',
|
||||
outputLocales: ['en-US'],
|
||||
|
||||
@@ -64,6 +64,10 @@ When working with Linear issues:
|
||||
3. **Update issue status** when completing tasks using `mcp__linear-server__update_issue`
|
||||
4. **MUST add completion comment** using `mcp__linear-server__create_comment`
|
||||
|
||||
### Creating Issues
|
||||
|
||||
When creating new Linear issues using `mcp__linear-server__create_issue`, **MUST add the `claude code` label** to indicate the issue was created by Claude Code.
|
||||
|
||||
### Completion Comment (REQUIRED)
|
||||
|
||||
**Every time you complete an issue, you MUST add a comment summarizing the work done.** This is critical for:
|
||||
|
||||
+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'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ export const appBrowsers = {
|
||||
identifier: 'chat',
|
||||
keepAlive: true,
|
||||
minWidth: 400,
|
||||
path: '/chat',
|
||||
path: '/agent',
|
||||
showOnInit: true,
|
||||
titleBarStyle: 'hidden',
|
||||
vibrancy: 'under-window',
|
||||
@@ -72,7 +72,7 @@ export const windowTemplates = {
|
||||
allowMultipleInstances: true,
|
||||
autoHideMenuBar: true,
|
||||
baseIdentifier: 'chatSingle',
|
||||
basePath: '/chat',
|
||||
basePath: '/agent',
|
||||
height: 600,
|
||||
keepAlive: false, // Multi-instance windows don't need to stay alive
|
||||
minWidth: 400,
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: >-
|
||||
LobeHub 2.0 is Here
|
||||
|
||||
description: >-
|
||||
LobeHub 2.0 is here, bringing a new level of AI collaboration and productivity.
|
||||
|
||||
tags:
|
||||
- LobeHub
|
||||
- AI Collaboration
|
||||
- Productivity
|
||||
---
|
||||
|
||||
# LobeHub 2.0 is Here 🎉
|
||||
|
||||
After nearly 10 days of meticulous refinement, LobeChat has fully integrated the DeepSeek R1 model in version v1.49.12, offering users a revolutionary interactive experience in the chain of thought!
|
||||
|
||||
## 🚀 Major Updates
|
||||
|
||||
- 🤯 **Comprehensive Support for DeepSeek R1**: Now fully integrated in both the Community and Cloud versions ([lobechat.com](https://lobechat.com)).
|
||||
- 🧠 **Real-Time Chain of Thought Display**: Transparently presents the AI's reasoning process, making the resolution of complex issues clear and visible.
|
||||
- ⚡️ **Deep Thinking Experience**: Utilizing Chain of Thought technology, it provides more insightful AI conversations.
|
||||
- 💫 **Intuitive Problem Analysis**: Makes the analysis of complex issues clear and easy to understand.
|
||||
|
||||
## 🌟 How to Use
|
||||
|
||||
1. Upgrade to LobeChat v1.49.12 or visit [lobechat.com](https://lobechat.com).
|
||||
2. Select the DeepSeek R1 model in the settings.
|
||||
3. Experience a whole new level of intelligent conversation!
|
||||
|
||||
## 📢 Feedback and Support
|
||||
|
||||
If you encounter any issues while using the application or have suggestions for new features, feel free to engage with us through GitHub Discussions. Let's work together to create a better LobeChat!
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: >-
|
||||
LobeHub 2.0 is Here
|
||||
|
||||
description: >-
|
||||
LobeHub 2.0 is here, bringing a new level of AI collaboration and productivity.
|
||||
|
||||
tags:
|
||||
- LobeHub
|
||||
- AI Collaboration
|
||||
- Productivity
|
||||
---
|
||||
|
||||
# LobeHub 2.0 is Here 🎉
|
||||
|
||||
After nearly 10 days of meticulous refinement, LobeChat has fully integrated the DeepSeek R1 model in version v1.49.12, offering users a revolutionary interactive experience in the chain of thought!
|
||||
|
||||
## 🚀 Major Updates
|
||||
|
||||
- 🤯 **Comprehensive Support for DeepSeek R1**: Now fully integrated in both the Community and Cloud versions ([lobechat.com](https://lobechat.com)).
|
||||
- 🧠 **Real-Time Chain of Thought Display**: Transparently presents the AI's reasoning process, making the resolution of complex issues clear and visible.
|
||||
- ⚡️ **Deep Thinking Experience**: Utilizing Chain of Thought technology, it provides more insightful AI conversations.
|
||||
- 💫 **Intuitive Problem Analysis**: Makes the analysis of complex issues clear and easy to understand.
|
||||
|
||||
## 🌟 How to Use
|
||||
|
||||
1. Upgrade to LobeChat v1.49.12 or visit [lobechat.com](https://lobechat.com).
|
||||
2. Select the DeepSeek R1 model in the settings.
|
||||
3. Experience a whole new level of intelligent conversation!
|
||||
|
||||
## 📢 Feedback and Support
|
||||
|
||||
If you encounter any issues while using the application or have suggestions for new features, feel free to engage with us through GitHub Discussions. Let's work together to create a better LobeChat!
|
||||
@@ -2,6 +2,12 @@
|
||||
"$schema": "https://github.com/lobehub/lobe-chat/blob/main/docs/changelog/schema.json",
|
||||
"cloud": [],
|
||||
"community": [
|
||||
{
|
||||
"image": "https://github.com/user-attachments/assets/5fe4c373-ebd0-42a9-bdca-0ab7e0a2e747",
|
||||
"id": "2025-12-15-V2",
|
||||
"date": "2025-12-15",
|
||||
"versionRange": ["1.47.8", "1.49.12"]
|
||||
},
|
||||
{
|
||||
"image": "https://github.com/user-attachments/assets/5fe4c373-ebd0-42a9-bdca-0ab7e0a2e747",
|
||||
"id": "2025-02-02-deepseek-r1",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"助手": {
|
||||
"en-US": "Agent"
|
||||
},
|
||||
"文稿": {
|
||||
"en-US": "Page"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { KnipConfig } from 'knip';
|
||||
|
||||
const config: KnipConfig = {
|
||||
entry: ['src/app/**/*.ts{x,}'],
|
||||
ignore: [
|
||||
// Test files
|
||||
'src/**/__tests__/**',
|
||||
'src/**/*.test.ts{x,}',
|
||||
'src/**/*.spec.ts{x,}',
|
||||
// Other directories
|
||||
'packages/**',
|
||||
'e2e/**',
|
||||
'scripts/**',
|
||||
// Config files
|
||||
'*.config.{js,ts,mjs,cjs}',
|
||||
'next-env.d.ts',
|
||||
],
|
||||
ignoreDependencies: [],
|
||||
ignoreExportsUsedInFile: true,
|
||||
project: ['src/**/*.ts{x,}'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
+12
-4
@@ -133,7 +133,7 @@
|
||||
"passwordPlaceholder": "يرجى إدخال كلمة المرور",
|
||||
"signinLink": "تسجيل الدخول الآن",
|
||||
"submit": "تسجيل",
|
||||
"subtitle": "انضم إلى مجتمع LobeChat",
|
||||
"subtitle": "ابدأ مساحة التعاون الخاصة بـ Agents",
|
||||
"success": "تم التسجيل بنجاح! يرجى التحقق من بريدك الإلكتروني لتأكيد الحساب",
|
||||
"title": "إنشاء حساب",
|
||||
"usernamePlaceholder": "يرجى إدخال اسم المستخدم"
|
||||
@@ -141,8 +141,7 @@
|
||||
"verifyEmail": {
|
||||
"backToSignIn": "العودة إلى تسجيل الدخول",
|
||||
"checkSpam": "إذا لم تتلقَ البريد الإلكتروني، يرجى التحقق من مجلد الرسائل غير المرغوب فيها",
|
||||
"descriptionPrefix": "لقد أرسلنا رسالة تحقق إلى",
|
||||
"descriptionSuffix": "",
|
||||
"description": "تم إرسال رسالة تحقق إلى {{email}}",
|
||||
"resend": {
|
||||
"button": "إعادة إرسال رسالة التحقق",
|
||||
"error": "فشل الإرسال، يرجى المحاولة لاحقًا",
|
||||
@@ -156,6 +155,11 @@
|
||||
"prevMonth": "الشهر الماضي",
|
||||
"recent30Days": "آخر 30 يومًا"
|
||||
},
|
||||
"footer": {
|
||||
"agreement": "بالمتابعة، فإنك تؤكد أنك قد قرأت ووافقت على <terms>الشروط والأحكام</terms> و<privacy>سياسة الخصوصية</privacy>",
|
||||
"privacy": "سياسة الخصوصية",
|
||||
"terms": "شروط الخدمة"
|
||||
},
|
||||
"header": {
|
||||
"desc": "إدارة معلومات حسابك.",
|
||||
"title": "الحساب"
|
||||
@@ -221,6 +225,10 @@
|
||||
"usernameRule": "اسم المستخدم يجب أن يحتوي فقط على أحرف أو أرقام أو شرطة سفلية",
|
||||
"usernameUpdateFailed": "فشل في تحديث اسم المستخدم، يرجى المحاولة لاحقًا"
|
||||
},
|
||||
"signin": {
|
||||
"subtitle": "سجّل أو قم بتسجيل الدخول إلى حسابك في {{appName}}",
|
||||
"title": "مساحة التعاون الخاصة بك في Agents"
|
||||
},
|
||||
"signout": "تسجيل الخروج",
|
||||
"signup": "الاشتراك",
|
||||
"stats": {
|
||||
@@ -266,7 +274,7 @@
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "إدارة مفاتيح API",
|
||||
"profile": "الملف الشخصي",
|
||||
"profile": "حسابي",
|
||||
"security": "الأمان",
|
||||
"stats": "الإحصائيات",
|
||||
"usage": "إحصاءات الاستخدام"
|
||||
|
||||
+23
-16
@@ -22,13 +22,13 @@
|
||||
},
|
||||
"clearCurrentMessages": "مسح رسائل الجلسة الحالية",
|
||||
"confirmClearCurrentMessages": "سيتم مسح رسائل الجلسة الحالية قريبًا، وبمجرد المسح لن يمكن استعادتها، يرجى تأكيد الإجراء الخاص بك",
|
||||
"confirmRemoveChatGroupItemAlert": "سيتم حذف فريق الوكيل هذا، ولن يتأثر الأعضاء الآخرون. يرجى تأكيد الإجراء.",
|
||||
"confirmRemoveChatGroupItemAlert": "سيتم حذف هذه المجموعة، ولن يتأثر أعضاء الفريق. يرجى تأكيد الإجراء.",
|
||||
"confirmRemoveGroupItemAlert": "سيتم حذف هذه المجموعة قريبًا. بعد الحذف، سيُنتقل المساعدون في هذه المجموعة إلى القائمة الافتراضية. يرجى تأكيد إجراء الحذف.",
|
||||
"confirmRemoveGroupSuccess": "تم حذف فريق الوكلاء بنجاح",
|
||||
"confirmRemoveGroupSuccess": "تم حذف المجموعة بنجاح",
|
||||
"confirmRemoveSessionItemAlert": "سيتم حذف هذا المساعد قريبًا، وبمجرد الحذف لن يمكن استعادته، يرجى تأكيد الإجراء الخاص بك",
|
||||
"confirmRemoveSessionSuccess": "تم حذف المساعد بنجاح",
|
||||
"defaultAgent": "المساعد الافتراضي",
|
||||
"defaultGroupChat": "فريق الوكلاء",
|
||||
"defaultGroupChat": "مجموعة",
|
||||
"defaultList": "القائمة الافتراضية",
|
||||
"defaultSession": "المساعد الافتراضي",
|
||||
"dm": {
|
||||
@@ -44,6 +44,7 @@
|
||||
},
|
||||
"duplicateTitle": "{{title}} نسخة",
|
||||
"emptyAgent": "لا يوجد مساعد",
|
||||
"emptyAgentAction": "إنشاء مساعد",
|
||||
"extendParams": {
|
||||
"disableContextCaching": {
|
||||
"desc": "يمكن تقليل تكلفة توليد محادثة واحدة بنسبة تصل إلى 90%، وزيادة سرعة الاستجابة بمقدار 4 مرات (<1>اعرف المزيد</1>). عند التفعيل، سيتم تعطيل حد عدد الرسائل التاريخية تلقائيًا",
|
||||
@@ -120,7 +121,7 @@
|
||||
"noTemplateMembers": "لا يوجد أعضاء في القالب",
|
||||
"noTemplates": "لا توجد قوالب متاحة",
|
||||
"searchTemplates": "ابحث في القوالب...",
|
||||
"title": "إنشاء فريق وكلاء",
|
||||
"title": "إنشاء مجموعة",
|
||||
"useTemplate": "استخدام القالب"
|
||||
},
|
||||
"hideForYou": "تم إخفاء محتوى الرسائل الخاصة، يرجى تفعيل خيار 【عرض محتوى الرسائل الخاصة】 في الإعدادات للعرض",
|
||||
@@ -154,25 +155,25 @@
|
||||
"knowledgeBase": {
|
||||
"all": "جميع المحتويات",
|
||||
"allFiles": "جميع الملفات",
|
||||
"allKnowledgeBases": "جميع قواعد المعرفة",
|
||||
"disabled": "الوضع الحالي للنشر لا يدعم محادثات قاعدة المعرفة. إذا كنت بحاجة إلى استخدامها، يرجى التبديل إلى نشر قاعدة البيانات على الخادم أو استخدام خدمة {{cloud}}.",
|
||||
"allLibraries": "جميع قواعد البيانات",
|
||||
"disabled": "وضع النشر الحالي لا يدعم المحادثة مع قاعدة البيانات. لاستخدام هذه الميزة، يرجى التبديل إلى نشر قاعدة بيانات على الخادم أو استخدام خدمة {{cloud}}",
|
||||
"library": {
|
||||
"action": {
|
||||
"add": "إضافة",
|
||||
"detail": "تفاصيل",
|
||||
"remove": "إزالة"
|
||||
},
|
||||
"title": "الملفات/قاعدة المعرفة"
|
||||
"title": "الملفات / قاعدة البيانات"
|
||||
},
|
||||
"relativeFilesOrKnowledgeBases": "ملفات/قواعد معرفة مرتبطة",
|
||||
"title": "قاعدة المعرفة",
|
||||
"uploadGuide": "يمكنك عرض الملفات التي تم تحميلها في «قاعدة المعرفة»",
|
||||
"relativeFilesOrLibraries": "الملفات / قواعد البيانات المرتبطة",
|
||||
"title": "قاعدة البيانات",
|
||||
"uploadGuide": "يمكنك عرض الملفات التي تم تحميلها في قسم \"الموارد\"",
|
||||
"viewMore": "عرض المزيد"
|
||||
},
|
||||
"memberSelection": {
|
||||
"addMember": "إضافة عضو",
|
||||
"allMembers": "جميع الأعضاء",
|
||||
"createGroup": "إنشاء فريق وكيل",
|
||||
"createGroup": "إنشاء مجموعة",
|
||||
"noAvailableAgents": "لا يوجد وكلاء متاحون للدعوة",
|
||||
"noSelectedAgents": "لم يتم اختيار أي وكيل بعد",
|
||||
"searchAgents": "ابحث عن وكيل...",
|
||||
@@ -245,14 +246,15 @@
|
||||
"senderAssistant": "الوكيل",
|
||||
"senderUser": "أنت"
|
||||
},
|
||||
"newAgent": "مساعد جديد",
|
||||
"newGroupChat": "إنشاء فريق وكلاء جديد",
|
||||
"noAgentsYet": "لا يوجد أعضاء في هذا الفريق بعد. انقر على زر + لدعوة مساعد.",
|
||||
"newAgent": "إنشاء مساعد",
|
||||
"newGroupChat": "إنشاء مجموعة",
|
||||
"newPage": "إنشاء مستند",
|
||||
"noAgentsYet": "لا يوجد أعضاء في هذه المجموعة بعد. انقر على زر + لدعوة مساعد.",
|
||||
"noAvailableAgents": "لا يوجد أعضاء متاحون للدعوة",
|
||||
"noMatchingAgents": "لا يوجد أعضاء مطابقون",
|
||||
"noMembersYet": "لا يوجد أعضاء في هذه المجموعة بعد. انقر على زر + لدعوة المساعدين.",
|
||||
"noSelectedAgents": "لم يتم اختيار أي أعضاء بعد",
|
||||
"openInNewWindow": "افتح الصفحة في نافذة جديدة",
|
||||
"openInNewWindow": "فتح في نافذة مستقلة",
|
||||
"owner": "مالك المجموعة",
|
||||
"pin": "تثبيت",
|
||||
"pinOff": "إلغاء التثبيت",
|
||||
@@ -296,7 +298,7 @@
|
||||
"searchAgentPlaceholder": "مساعد البحث...",
|
||||
"searchAgents": "مساعد البحث...",
|
||||
"selectedAgents": "المساعدون المختارون",
|
||||
"sendPlaceholder": "أدخل محتوى الدردشة...",
|
||||
"sendPlaceholder": "اطرح سؤالًا، أنشئ محتوى، أو ابدأ مهمة، <hotkey><hotkey/>",
|
||||
"sessionGroup": {
|
||||
"config": "إدارة المجموعات",
|
||||
"confirmRemoveGroupAlert": "سيتم حذف هذه المجموعة قريبًا، وبعد الحذف، سيتم نقل مساعدي هذه المجموعة إلى القائمة الافتراضية، يرجى تأكيد إجراءك",
|
||||
@@ -361,6 +363,10 @@
|
||||
"title": "المهام المنجزة"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"profile": "ملف المساعد",
|
||||
"search": "بحث"
|
||||
},
|
||||
"thread": {
|
||||
"divider": "موضوع فرعي",
|
||||
"threadMessageCount": "{{messageCount}} رسالة",
|
||||
@@ -413,6 +419,7 @@
|
||||
"checkOpenNewTopic": "هل ترغب في فتح موضوع جديد؟",
|
||||
"checkSaveCurrentMessages": "هل ترغب في حفظ الدردشة الحالية كموضوع؟",
|
||||
"openNewTopic": "فتح موضوع جديد",
|
||||
"recent": "المواضيع الأخيرة",
|
||||
"saveCurrentMessages": "حفظ الجلسة الحالية كموضوع"
|
||||
},
|
||||
"translate": {
|
||||
|
||||
+29
-2
@@ -137,6 +137,8 @@
|
||||
"close": "إغلاق",
|
||||
"cmdk": {
|
||||
"about": "حول",
|
||||
"aiModeEmptyState": "أدخل سؤالك في الحقل أعلاه لبدء المحادثة مع الذكاء الاصطناعي",
|
||||
"aiModePlaceholder": "اطرح سؤالاً على الذكاء الاصطناعي...",
|
||||
"communitySupport": "دعم المجتمع",
|
||||
"discover": "استكشاف",
|
||||
"knowledgeBase": "قاعدة المعرفة",
|
||||
@@ -145,6 +147,16 @@
|
||||
"noResults": "لم يتم العثور على نتائج",
|
||||
"openSettings": "فتح الإعدادات",
|
||||
"painting": "الرسم بالذكاء الاصطناعي",
|
||||
"search": {
|
||||
"agent": "مساعد",
|
||||
"agents": "مساعدون",
|
||||
"file": "ملف",
|
||||
"files": "ملفات",
|
||||
"loading": "جارٍ البحث...",
|
||||
"searching": "نتائج البحث",
|
||||
"topic": "موضوع",
|
||||
"topics": "مواضيع"
|
||||
},
|
||||
"searchPlaceholder": "أدخل أمرًا أو ابحث...",
|
||||
"settings": "الإعدادات",
|
||||
"starOnGitHub": "قيّمنا على GitHub",
|
||||
@@ -304,6 +316,13 @@
|
||||
"business": "شراكات تجارية",
|
||||
"support": "الدعم عبر البريد الإلكتروني"
|
||||
},
|
||||
"navPanel": {
|
||||
"agent": "المساعد",
|
||||
"displayItems": "عرض العناصر",
|
||||
"library": "المكتبة",
|
||||
"searchAgent": "بحث عن مساعد...",
|
||||
"searchResultEmpty": "لا توجد نتائج بحث"
|
||||
},
|
||||
"new": "جديد",
|
||||
"oauth": "تسجيل الدخول SSO",
|
||||
"officialSite": "الموقع الرسمي",
|
||||
@@ -358,13 +377,21 @@
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"aiImage": "الرسم بالذكاء الاصطناعي",
|
||||
"aiImage": "رسم",
|
||||
"audio": "الصوت",
|
||||
"chat": "الدردشة",
|
||||
"community": "المجتمع",
|
||||
"discover": "اكتشاف",
|
||||
"files": "ملفات",
|
||||
"home": "الصفحة الرئيسية",
|
||||
"knowledgeBase": "قاعدة المعرفة",
|
||||
"me": "أنا",
|
||||
"setting": "الإعدادات"
|
||||
"memory": "الذاكرة",
|
||||
"pages": "المستندات",
|
||||
"resource": "الموارد",
|
||||
"search": "البحث",
|
||||
"setting": "الإعدادات",
|
||||
"video": "الفيديو"
|
||||
},
|
||||
"telemetry": {
|
||||
"allow": "السماح",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"chunkingTooltip": "قم بتقسيم الملف إلى عدة كتل نصية وتحويلها إلى متجهات، يمكن استخدامها في البحث الدلالي والمحادثة حول الملفات",
|
||||
"chunkingUnsupported": "هذا الملف لا يدعم تقسيم الأجزاء",
|
||||
"confirmDelete": "سيتم حذف هذا الملف، ولن يمكن استعادته بعد الحذف، يرجى تأكيد العملية",
|
||||
"confirmDeleteFolder": "سيتم حذف هذا المجلد وجميع محتوياته، ولن يكون بالإمكان استعادته بعد الحذف. يرجى تأكيد العملية.",
|
||||
"confirmDeleteMultiFiles": "سيتم حذف {{count}} ملفًا محددًا، ولن يمكن استعادته بعد الحذف، يرجى تأكيد العملية",
|
||||
"confirmRemoveFromKnowledgeBase": "سيتم إزالة {{count}} ملفًا محددًا من قاعدة المعرفة، لا يزال بإمكانك رؤية الملفات في جميع الملفات، يرجى تأكيد العملية",
|
||||
"copyUrl": "نسخ الرابط",
|
||||
@@ -26,8 +27,19 @@
|
||||
"createChunkingTask": "جارٍ التحضير...",
|
||||
"deleteSuccess": "تم حذف الملف بنجاح",
|
||||
"downloading": "جارٍ تحميل الملف...",
|
||||
"goBack": "العودة إلى الصفحة السابقة",
|
||||
"goForward": "الانتقال إلى الصفحة التالية",
|
||||
"goToParent": "الانتقال إلى المجلد الرئيسي",
|
||||
"moveError": "فشل في نقل الملف",
|
||||
"moveHere": "نقل إلى هنا",
|
||||
"moveSuccess": "تم نقل الملف بنجاح",
|
||||
"moveToFolder": "نقل إلى...",
|
||||
"moveToRoot": "نقل إلى الدليل الجذري",
|
||||
"removeFromKnowledgeBase": "إزالة من قاعدة المعرفة",
|
||||
"removeFromKnowledgeBaseSuccess": "تمت إزالة الملف بنجاح"
|
||||
"removeFromKnowledgeBaseSuccess": "تمت إزالة الملف بنجاح",
|
||||
"rename": "إعادة التسمية",
|
||||
"renameError": "فشل في إعادة التسمية",
|
||||
"renameSuccess": "تمت إعادة التسمية بنجاح"
|
||||
},
|
||||
"bottom": "لقد وصلت إلى النهاية",
|
||||
"config": {
|
||||
@@ -42,6 +54,12 @@
|
||||
"or": "أو",
|
||||
"title": "قم بسحب الملف أو المجلد هنا"
|
||||
},
|
||||
"noFolders": "لا توجد مجلدات حالياً",
|
||||
"sort": {
|
||||
"dateAdded": "تاريخ الإضافة",
|
||||
"name": "الاسم",
|
||||
"size": "الحجم"
|
||||
},
|
||||
"title": {
|
||||
"createdAt": "تاريخ الإنشاء",
|
||||
"size": "الحجم",
|
||||
|
||||
@@ -60,9 +60,9 @@
|
||||
},
|
||||
"list": "قائمة المساعدين",
|
||||
"marketSource": {
|
||||
"label": "تبديل مصدر السوق",
|
||||
"legacy": "السوق القديم",
|
||||
"new": "السوق الجديد"
|
||||
"label": "تبديل مصدر المجتمع",
|
||||
"legacy": "المجتمع القديم",
|
||||
"new": "المجتمع الجديد"
|
||||
},
|
||||
"more": "المزيد",
|
||||
"plugins": "دمج الإضافات",
|
||||
@@ -85,7 +85,7 @@
|
||||
"subtitle": "المساعد الذي تحاول الوصول إليه تم أرشفته للأسباب التالية المحتملة:",
|
||||
"title": "تم أرشفة المساعد"
|
||||
},
|
||||
"backToMarket": "العودة إلى سوق المساعدين",
|
||||
"backToMarket": "العودة إلى مجتمع المساعد",
|
||||
"deprecated": {
|
||||
"reasons": {
|
||||
"official": "تمت إزالة المساعد من قبل الإدارة لأسباب أمنية أو سياسية",
|
||||
@@ -94,9 +94,9 @@
|
||||
"subtitle": "المساعد الذي تحاول الوصول إليه تم رفضه للأسباب التالية المحتملة:",
|
||||
"title": "تم رفض المساعد"
|
||||
},
|
||||
"support": "إذا واجهت أي مشاكل، يرجى نسخ الرابط وإرساله إلى <1>support@lobehub.com</1> للاستفسار.",
|
||||
"support": "لأي استفسارات، يرجى نسخ الرابط وإرساله إلى <email>support@lobehub.com</email>.",
|
||||
"unpublished": {
|
||||
"subtitle": "المساعد الذي تحاول الوصول إليه قيد المراجعة حاليًا. إذا كان لديك أي استفسار، يرجى نسخ الرابط وإرساله إلى <1>support@lobehub.com</1>.",
|
||||
"subtitle": "المساعد الذي تحاول الوصول إليه يخضع حاليًا لمراجعة الإصدار. إذا كان لديك أي استفسار، يرجى نسخ الرابط وإرساله إلى <email>support@lobehub.com</email>.",
|
||||
"title": "المساعد قيد المراجعة"
|
||||
}
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
"createGuide": {
|
||||
"func1": {
|
||||
"desc1": "ادخل إلى صفحة إعداد المساعد الذي ترغب في تقديمه من خلال الإعدادات في الزاوية العليا اليمنى من نافذة المحادثة;",
|
||||
"desc2": "انقر على زر التقديم إلى سوق المساعدين في الزاوية العليا اليمنى.",
|
||||
"desc2": "انقر على زر الإرسال إلى مجتمع المساعد في الزاوية اليمنى العليا.",
|
||||
"tag": "الطريقة الأولى",
|
||||
"title": "تقديم عبر LobeChat"
|
||||
},
|
||||
@@ -186,8 +186,10 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"communityAgents": "مساعدو المجتمع",
|
||||
"featuredAssistants": "مساعدون مميزون",
|
||||
"featuredModels": "نماذج مميزة",
|
||||
"featuredPlugins": "الإضافات المميزة",
|
||||
"featuredProviders": "مزودو نماذج مميزون",
|
||||
"featuredTools": "إضافات مميزة",
|
||||
"more": "اكتشف المزيد"
|
||||
@@ -516,7 +518,7 @@
|
||||
"hero": {
|
||||
"desc": "منصة MCP Servers مفتوحة المصدر وقابلة للنشر، تساعد أنظمة الذكاء الاصطناعي على الوصول بسهولة إلى أنظمة الملفات، قواعد البيانات، واجهات برمجة التطبيقات وغيرها من الموارد الحيوية، لتوسيع قدرات الذكاء الاصطناعي الخاصة بك بشكل شامل.",
|
||||
"subTitle": "مفتوح المصدر وجاهز للاستخدام",
|
||||
"title": "سوق MCP مفتوح المصدر للذكاء الاصطناعي"
|
||||
"title": "مجتمع MCP مفتوح المصدر للذكاء الاصطناعي"
|
||||
},
|
||||
"sorts": {
|
||||
"createdAt": "أضيف مؤخراً",
|
||||
@@ -529,7 +531,7 @@
|
||||
"toolsCount": "عدد الأدوات",
|
||||
"updatedAt": "تم التحديث مؤخراً"
|
||||
},
|
||||
"title": "سوق MCP",
|
||||
"title": "مجتمع MCP",
|
||||
"unvalidated": {
|
||||
"desc": "هذا الخادم MCP لم يتم التحقق منه بعد",
|
||||
"title": "غير مُحقق"
|
||||
|
||||
+17
-1
@@ -9,6 +9,11 @@
|
||||
"on": "إظهار شريط أدوات التنسيق"
|
||||
}
|
||||
},
|
||||
"autoSave": {
|
||||
"latest": "تم تحميل أحدث إصدار",
|
||||
"saved": "تم الحفظ",
|
||||
"saving": "يتم الحفظ تلقائيًا..."
|
||||
},
|
||||
"cancel": "إلغاء",
|
||||
"confirm": "تأكيد",
|
||||
"file": {
|
||||
@@ -20,10 +25,18 @@
|
||||
},
|
||||
"link": {
|
||||
"edit": "تعديل الرابط",
|
||||
"editLinkTitle": "الرابط",
|
||||
"editTextTitle": "العنوان",
|
||||
"open": "فتح الرابط",
|
||||
"placeholder": "أدخل عنوان URL للرابط",
|
||||
"unlink": "إزالة الرابط"
|
||||
},
|
||||
"markdown": {
|
||||
"cancel": "إلغاء",
|
||||
"confirm": "تحويل",
|
||||
"parseMessage": "سيتم تحويل المحتوى إلى تنسيق Markdown، وسيتم استبدال المحتوى الحالي. هل ترغب في المتابعة؟ (سيُغلق تلقائيًا بعد 5 ثوانٍ)",
|
||||
"parseTitle": "تنسيق Markdown"
|
||||
},
|
||||
"math": {
|
||||
"placeholder": "يرجى إدخال معادلة TeX"
|
||||
},
|
||||
@@ -50,13 +63,16 @@
|
||||
"bulletList": "قائمة نقطية",
|
||||
"code": "كود مضمن",
|
||||
"codeblock": "كتلة كود",
|
||||
"image": "صورة",
|
||||
"italic": "مائل",
|
||||
"link": "رابط",
|
||||
"numberList": "قائمة مرقمة",
|
||||
"redo": "إعادة",
|
||||
"strikethrough": "شطب",
|
||||
"table": "جدول",
|
||||
"taskList": "قائمة المهام",
|
||||
"tex": "معادلة TeX",
|
||||
"underline": "تسطير"
|
||||
"underline": "تسطير",
|
||||
"undo": "تراجع"
|
||||
}
|
||||
}
|
||||
|
||||
+34
-15
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"addFolder": "إنشاء مجلد",
|
||||
"addKnowledge": "إضافة معرفة",
|
||||
"addLibrary": "أضف إلى المكتبة",
|
||||
"addPage": "إنشاء مستند",
|
||||
"desc": "نظّم معرفتك في العمل، الدراسة والحياة.",
|
||||
"desc": "قم بإدارة مواردك للعمل، الدراسة والحياة.",
|
||||
"detail": {
|
||||
"basic": {
|
||||
"createdAt": "تاريخ الإنشاء",
|
||||
@@ -50,6 +50,12 @@
|
||||
"pin": "تثبيت المستند"
|
||||
},
|
||||
"saving": "جارٍ الحفظ...",
|
||||
"slashCommands": {
|
||||
"bulletedList": "قائمة غير مرتبة",
|
||||
"image": "صورة",
|
||||
"orderedList": "قائمة مرتبة",
|
||||
"todoList": "قائمة المهام"
|
||||
},
|
||||
"titlePlaceholder": "بدون عنوان",
|
||||
"wordCount": "{{wordCount}} كلمة"
|
||||
},
|
||||
@@ -57,14 +63,27 @@
|
||||
"copyContent": "نسخ المحتوى الكامل",
|
||||
"duplicate": "إنشاء نسخة",
|
||||
"empty": "لا توجد مستندات حالياً، انقر على الزر أعلاه لإنشاء أول مستند لك",
|
||||
"filter": {
|
||||
"all": "الكل",
|
||||
"onlyInPages": "فقط في المستندات"
|
||||
},
|
||||
"noResults": "لم يتم العثور على مستندات مطابقة",
|
||||
"pageCount": "إجمالي {{count}} مستند",
|
||||
"selectNote": "اختر مستندًا لبدء التحرير",
|
||||
"title": "المستندات",
|
||||
"untitled": "بدون عنوان"
|
||||
},
|
||||
"empty": "لا توجد ملفات/مجلدات تم تحميلها بعد",
|
||||
"header": {
|
||||
"actions": {
|
||||
"connect": "اتصال...",
|
||||
"gitignore": {
|
||||
"apply": "تطبيق القواعد",
|
||||
"cancel": "تجاهل القواعد",
|
||||
"content": "تم اكتشاف ملف .gitignore (عدد {{count}} من الملفات)، هل ترغب في تطبيق قواعد التجاهل؟",
|
||||
"filtered": "{{ignored}} ملفًا تم تجاهله من أصل {{total}} ملفًا",
|
||||
"title": "تم اكتشاف .gitignore"
|
||||
},
|
||||
"newFolder": "إنشاء مجلد جديد",
|
||||
"newPage": "مستند جديد",
|
||||
"uploadFile": "رفع ملف",
|
||||
@@ -91,7 +110,7 @@
|
||||
"quickActions": "إجراءات سريعة",
|
||||
"recentFiles": "الملفات الأخيرة",
|
||||
"recentPages": "الصفحات الأخيرة",
|
||||
"subtitle": "مرحبًا بك في قاعدة المعرفة، ابدأ من هنا لإدارة مستنداتك وملاحظاتك",
|
||||
"subtitle": "مرحبًا بك في مركز الموارد، ابدأ من هنا لإدارة مستنداتك وملفاتك.",
|
||||
"uploadEntries": {
|
||||
"files": {
|
||||
"title": "رفع ملفات"
|
||||
@@ -99,27 +118,27 @@
|
||||
"folder": {
|
||||
"title": "رفع مجلد"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "قاعدة معرفة جديدة"
|
||||
"library": {
|
||||
"title": "إنشاء مكتبة جديدة"
|
||||
},
|
||||
"newPage": {
|
||||
"title": "إنشاء مستند جديد"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"library": {
|
||||
"list": {
|
||||
"confirmRemoveKnowledgeBase": "سيتم حذف هذه المكتبة المعرفية، ولن يتم حذف الملفات الموجودة بها، بل ستنتقل إلى جميع الملفات. بعد حذف المكتبة المعرفية، لن يمكن استعادتها، يرجى توخي الحذر.",
|
||||
"empty": "انقر على <1>+</1> لبدء إنشاء مكتبة معرفية"
|
||||
"confirmRemoveLibrary": "سيتم حذف هذه المكتبة، لكن الملفات بداخلها لن تُحذف، بل سيتم نقلها إلى جميع الملفات. لا يمكن استعادة المكتبة بعد حذفها، يرجى الحذر.",
|
||||
"empty": "انقر <1>+</1> لبدء إنشاء مكتبة"
|
||||
},
|
||||
"new": "إنشاء مكتبة معرفية جديدة",
|
||||
"title": "المكتبة المعرفية"
|
||||
"new": "مكتبة",
|
||||
"title": "المكتبة"
|
||||
},
|
||||
"menu": {
|
||||
"allFiles": "جميع الملفات",
|
||||
"allPages": "جميع المستندات"
|
||||
},
|
||||
"networkError": "فشل في الحصول على قاعدة المعرفة، يرجى التحقق من اتصال الشبكة ثم إعادة المحاولة",
|
||||
"networkError": "فشل في تحميل المكتبة، يرجى التحقق من اتصال الشبكة والمحاولة مرة أخرى",
|
||||
"notSupportGuide": {
|
||||
"desc": "الوضع الحالي للنشر هو وضع قاعدة بيانات العميل، ولا يمكن استخدام وظيفة إدارة الملفات. يرجى التبديل إلى <1>وضع نشر قاعدة بيانات الخادم</1>، أو استخدام <3>LobeChat Cloud</3> مباشرة.",
|
||||
"features": {
|
||||
@@ -131,9 +150,9 @@
|
||||
"desc": "استخدام نماذج متجهات عالية الأداء لتحويل النصوص إلى متجهات، مما يتيح البحث الدلالي في محتوى الملفات.",
|
||||
"title": "تحويل دلالي إلى متجهات"
|
||||
},
|
||||
"repos": {
|
||||
"desc": "يدعم إنشاء مكتبات معرفية، ويسمح بإضافة أنواع مختلفة من الملفات، لبناء معرفتك في مجالك.",
|
||||
"title": "المكتبة المعرفية"
|
||||
"libraries": {
|
||||
"desc": "يدعم إنشاء مكتبات ويسمح بإضافة أنواع مختلفة من الملفات لبناء مواردك المتخصصة",
|
||||
"title": "المكتبة"
|
||||
}
|
||||
},
|
||||
"title": "الوضع الحالي للنشر لا يدعم إدارة الملفات"
|
||||
@@ -155,7 +174,7 @@
|
||||
"videos": "الفيديوهات",
|
||||
"websites": "المواقع"
|
||||
},
|
||||
"title": "قاعدة المعرفة",
|
||||
"title": "الموارد",
|
||||
"toggleLeftPanel": "إظهار/إخفاء اللوحة الجانبية اليسرى",
|
||||
"uploadDock": {
|
||||
"body": {
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
"desc": "إعادة توليد آخر رسالة",
|
||||
"title": "إعادة توليد الرسالة"
|
||||
},
|
||||
"saveDocument": {
|
||||
"desc": "احفظ جميع التغييرات التي أُجريت على المستند الحالي فورًا",
|
||||
"title": "حفظ المستند"
|
||||
},
|
||||
"saveTopic": {
|
||||
"desc": "حفظ الموضوع الحالي وفتح موضوع جديد",
|
||||
"title": "فتح موضوع جديد"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"addToKnowledgeBase": {
|
||||
"addSuccess": "تم إضافة الملف بنجاح، <1>عرض الآن</1>",
|
||||
"confirm": "إضافة",
|
||||
"error": "فشل في إضافة الملف إلى قاعدة المعرفة",
|
||||
"id": {
|
||||
"placeholder": "يرجى اختيار قاعدة المعرفة لإضافتها",
|
||||
"required": "يرجى اختيار قاعدة المعرفة",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"authorize": {
|
||||
"cancel": "إلغاء",
|
||||
"confirm": "تأكيد التفويض",
|
||||
"description": {
|
||||
"and": "و",
|
||||
"prefix": "بالنقر على تأكيد التفويض، فإنك توافق على",
|
||||
"confirm": "أنشئ ملفك الشخصي",
|
||||
"description": "ملفك الشخصي في المجتمع مستقل عن حساب المستخدم في {{appName}}.",
|
||||
"footer": {
|
||||
"agreement": "بمتابعتك، فإنك تؤكد أنك قد قرأت ووافقت على <terms>الشروط والأحكام</terms> و<privacy>سياسة الخصوصية</privacy>",
|
||||
"privacy": "سياسة الخصوصية",
|
||||
"terms": "شروط الخدمة"
|
||||
},
|
||||
"title": "تأكيد التفويض"
|
||||
"subtitle": "أنشئ ملفًا شخصيًا في المجتمع لتتمكن من تقديم وإدارة معلومات النشر.",
|
||||
"title": "إنشاء ملف المجتمع الشخصي"
|
||||
},
|
||||
"callback": {
|
||||
"buttons": {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"identity": {
|
||||
"empty": "لا توجد ذاكرة هوية حالياً",
|
||||
"filter": {
|
||||
"search": "ابحث عن دور أو علاقة أو وصف...",
|
||||
"type": {
|
||||
"all": "الكل",
|
||||
"demographic": "التركيبة السكانية",
|
||||
"personal": "شخصي",
|
||||
"professional": "مهني"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"confirmDelete": "تأكيد الحذف",
|
||||
"deleteCancel": "إلغاء",
|
||||
"deleteContent": "هل أنت متأكد من أنك تريد حذف ذاكرة الهوية هذه؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"deleteOk": "حذف",
|
||||
"noResults": "لم يتم العثور على أي ذاكرة هوية مطابقة",
|
||||
"updated": "تم التحديث"
|
||||
},
|
||||
"roleCloud": {
|
||||
"collapse": "إخفاء",
|
||||
"expand": "عرض المزيد"
|
||||
},
|
||||
"view": {
|
||||
"list": "قائمة",
|
||||
"timeline": "الجدول الزمني"
|
||||
}
|
||||
},
|
||||
"loading": "جارٍ التحميل..."
|
||||
}
|
||||
@@ -10,24 +10,24 @@
|
||||
"discover": {
|
||||
"assistants": {
|
||||
"description": "إنشاء المحتوى، الكتابة، الأسئلة والأجوبة، توليد الصور، توليد الفيديو، توليد الصوت، الوكلاء الذكيون، سير العمل الآلي، تخصيص مساعد الذكاء الاصطناعي / GPTs / OLLaMA الخاص بك",
|
||||
"title": "مساعدات الذكاء الاصطناعي"
|
||||
"title": "مجتمع الوكلاء الذكيين"
|
||||
},
|
||||
"description": "إنشاء المحتوى، الكتابة، الأسئلة والأجوبة، توليد الصور، توليد الفيديو، توليد الصوت، الوكلاء الذكيون، سير العمل الآلي، تطبيقات الذكاء الاصطناعي المخصصة، تخصيص منصة تطبيقات الذكاء الاصطناعي الخاصة بك",
|
||||
"mcp": {
|
||||
"description": "ابحث وقارن واتصل بآلاف خوادم MCP، مما يساعد أنظمة الذكاء الاصطناعي على الوصول بسهولة إلى أنظمة الملفات وقواعد البيانات وواجهات برمجة التطبيقات وغيرها من الموارد الحيوية، لتوسيع قدرات الذكاء الاصطناعي الخاصة بك بشكل شامل",
|
||||
"title": "سوق خوادم MCP"
|
||||
"title": "مجتمع خوادم MCP"
|
||||
},
|
||||
"models": {
|
||||
"description": "استكشاف نماذج الذكاء الاصطناعي الرائجة OpenAI / GPT / Claude 3 / Gemini / Ollama / Azure / DeepSeek",
|
||||
"title": "نماذج الذكاء الاصطناعي"
|
||||
"title": "مجتمع النماذج"
|
||||
},
|
||||
"plugins": {
|
||||
"description": "استكشف توليد الرسوم البيانية، والأبحاث الأكاديمية، وتوليد الصور، وتوليد الفيديو، وتوليد الصوت، وأتمتة سير العمل، ودمج قدرات إضافية غنية لمساعدتك.",
|
||||
"title": "إضافات الذكاء الاصطناعي"
|
||||
"title": "مجتمع الإضافات"
|
||||
},
|
||||
"providers": {
|
||||
"description": "استكشاف مزودي النماذج الرائجة OpenAI / Qwen / Ollama / Anthropic / DeepSeek / Google Gemini / OpenRouter",
|
||||
"title": "مزودو خدمات نماذج الذكاء الاصطناعي"
|
||||
"title": "مجتمع مزودي النماذج"
|
||||
},
|
||||
"search": "بحث",
|
||||
"title": "اكتشاف"
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"plugins": {
|
||||
"description": "البحث، توليد الرسوم البيانية، الأكاديميات، توليد الصور، توليد الفيديو، توليد الصوت، سير العمل الآلي، خصص قدرات ToolCall الخاصة بـ ChatGPT / Claude",
|
||||
"title": "سوق الإضافات"
|
||||
"title": "مجتمع الإضافات"
|
||||
},
|
||||
"welcome": {
|
||||
"description": "{{appName}} يقدم لك أفضل تجربة لاستخدام ChatGPT وClaude وGemini وOLLaMA WebUI",
|
||||
|
||||
@@ -338,6 +338,8 @@
|
||||
"installed": "مثبت"
|
||||
},
|
||||
"config": {
|
||||
"addEnv": "إضافة متغير بيئة",
|
||||
"addHeaders": "إضافة رؤوس الطلب",
|
||||
"args": "المعلمات",
|
||||
"command": "الأمر",
|
||||
"env": "متغيرات البيئة",
|
||||
@@ -358,12 +360,15 @@
|
||||
},
|
||||
"title": "تثبيت إضافة مخصصة"
|
||||
},
|
||||
"install": {
|
||||
"title": "معلومات التثبيت"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "تثبيت إضافات الطرف الثالث",
|
||||
"trustedBy": "مقدم من {{name}}",
|
||||
"unverified": {
|
||||
"title": "إضافات طرف ثالث غير موثوقة",
|
||||
"warning": "هذه الإضافة من سوق طرف ثالث غير موثوق، يرجى التأكد من ثقتك بالمصدر قبل التثبيت."
|
||||
"warning": "هذا المكون الإضافي来自 مجتمع طرف ثالث غير موثوق به. يرجى التأكد من أنك تثق في هذا المصدر قبل التثبيت."
|
||||
},
|
||||
"verified": "موثوقة"
|
||||
},
|
||||
@@ -441,7 +446,7 @@
|
||||
"envConfigDescription": "سيتم تمرير هذه الإعدادات كمتغيرات بيئة عند بدء تشغيل خادم MCP",
|
||||
"httpTypeNotice": "إضافات MCP من نوع HTTP لا تحتاج إلى متغيرات بيئة للتكوين حاليًا",
|
||||
"indexUrl": {
|
||||
"title": "فهرس السوق",
|
||||
"title": "فهرس المجتمع",
|
||||
"tooltip": "لا يدعم التحرير عبر الإنترنت حاليًا، يرجى التكوين عبر متغيرات البيئة عند النشر"
|
||||
},
|
||||
"messages": {
|
||||
@@ -450,14 +455,14 @@
|
||||
"envUpdateFailed": "فشل حفظ متغيرات البيئة",
|
||||
"envUpdateSuccess": "تم حفظ متغيرات البيئة بنجاح"
|
||||
},
|
||||
"modalDesc": "بعد تكوين عنوان سوق الإضافات، يمكنك استخدام سوق إضافات مخصص",
|
||||
"modalDesc": "بعد تكوين عنوان مجتمع المكونات الإضافية، يمكنك استخدام مجتمع مكونات إضافية مخصص",
|
||||
"rules": {
|
||||
"argsRequired": "يرجى إدخال معلمات التشغيل",
|
||||
"commandRequired": "يرجى إدخال أمر التشغيل",
|
||||
"urlRequired": "يرجى إدخال عنوان الخدمة"
|
||||
},
|
||||
"saveSettings": "حفظ الإعدادات",
|
||||
"title": "إعدادات سوق الإضافات"
|
||||
"title": "إعداد مجتمع المكونات الإضافية"
|
||||
},
|
||||
"showInPortal": "يرجى عرض التفاصيل في مساحة العمل",
|
||||
"store": {
|
||||
|
||||
+36
-18
@@ -2,6 +2,7 @@
|
||||
"about": {
|
||||
"title": "حول"
|
||||
},
|
||||
"advancedSettings": "الإعدادات المتقدمة",
|
||||
"agentInfoDescription": {
|
||||
"basic": {
|
||||
"avatar": "الصورة الرمزية",
|
||||
@@ -76,6 +77,12 @@
|
||||
"title": "إعادة تعيين جميع الإعدادات"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"aiConfig": "إعدادات الذكاء الاصطناعي",
|
||||
"common": "عام",
|
||||
"profile": "الحساب",
|
||||
"system": "النظام"
|
||||
},
|
||||
"groupTab": {
|
||||
"chat": "الدردشة",
|
||||
"members": "الأعضاء",
|
||||
@@ -85,7 +92,7 @@
|
||||
"desc": "إعدادات التفضيلات والنماذج.",
|
||||
"global": "إعدادات عامة",
|
||||
"group": "إعدادات الفريق",
|
||||
"groupDesc": "إدارة فريق الوكلاء وتفضيلات المحادثة",
|
||||
"groupDesc": "إدارة المجموعات وتفضيلات الدردشة",
|
||||
"session": "إعدادات الجلسة",
|
||||
"sessionDesc": "إعداد الشخصية وتفضيلات الجلسة.",
|
||||
"sessionWithName": "إعدادات الجلسة · {{name}}",
|
||||
@@ -216,27 +223,28 @@
|
||||
"messages": {
|
||||
"createVersionFailed": "فشل إنشاء الإصدار: {{message}}",
|
||||
"fetchRemoteFailed": "فشل في جلب بيانات المساعد من السوق",
|
||||
"missingIdentifier": "لا يحتوي هذا المساعد على معرف سوق حتى الآن",
|
||||
"notAuthenticated": "يرجى تسجيل الدخول إلى حساب السوق أولاً",
|
||||
"missingIdentifier": "المساعد الحالي لا يحتوي على معرف المجتمع",
|
||||
"notAuthenticated": "يرجى تسجيل الدخول إلى حساب المجتمع أولاً",
|
||||
"publishFailed": "فشل النشر: {{message}}"
|
||||
},
|
||||
"submitButton": "نشر",
|
||||
"title": {
|
||||
"submit": "مشاركة في سوق المساعدين",
|
||||
"submit": "مشاركة في مجتمع المساعد",
|
||||
"upload": "نشر إصدار جديد"
|
||||
}
|
||||
},
|
||||
"resultModal": {
|
||||
"message": "تم إرسال المساعد للمراجعة، وسيتم نشره تلقائيًا بعد الموافقة. انقر على \"عرض في السوق\" لرؤية المساعد المنشور.",
|
||||
"view": "عرض في السوق"
|
||||
"message": "تم إرسال المساعد الذي أنشأته للمراجعة، وسيتم نشره تلقائيًا بعد الموافقة.",
|
||||
"title": "تم الإرسال بنجاح",
|
||||
"view": "الانتقال إلى المجتمع لعرضه"
|
||||
},
|
||||
"submit": {
|
||||
"button": "مشاركة في السوق",
|
||||
"tooltip": "شارك المساعد في سوق المساعدين"
|
||||
"button": "مشاركة في المجتمع",
|
||||
"tooltip": "شارك المساعد في المجتمع"
|
||||
},
|
||||
"upload": {
|
||||
"button": "نشر إصدار جديد",
|
||||
"tooltip": "نشر إصدار جديد في سوق المساعدين"
|
||||
"tooltip": "نشر إصدار جديد في مجتمع المساعد"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
@@ -259,6 +267,7 @@
|
||||
},
|
||||
"settingAgent": {
|
||||
"avatar": {
|
||||
"sizeExceeded": "تجاوز حجم الصورة الحد الأقصى المسموح به وهو 1 ميغابايت، يرجى اختيار صورة أصغر.",
|
||||
"title": "الصورة الرمزية"
|
||||
},
|
||||
"backgroundColor": {
|
||||
@@ -274,12 +283,12 @@
|
||||
"title": "الاسم"
|
||||
},
|
||||
"prompt": {
|
||||
"placeholder": "الرجاء إدخال كلمة الإشارة للشخصية",
|
||||
"title": "ضبط الشخصية"
|
||||
"placeholder": "أدخل إعدادات المساعد، اضغط / لفتح قائمة الأوامر",
|
||||
"title": "إعدادات المساعد"
|
||||
},
|
||||
"submit": "تحديث معلومات المساعد",
|
||||
"tag": {
|
||||
"desc": "سيتم عرض علامة المساعد في سوق المساعدين",
|
||||
"desc": "سيتم عرض وسم المساعد في مجتمع المساعد",
|
||||
"placeholder": "الرجاء إدخال العلامة",
|
||||
"title": "العلامة"
|
||||
},
|
||||
@@ -425,7 +434,7 @@
|
||||
"placeholder": "يرجى إدخال كلمة تلميح نظام المضيف",
|
||||
"title": "كلمة تلميح نظام المضيف"
|
||||
},
|
||||
"title": "معلومات فريق الوكلاء"
|
||||
"title": "معلومات المجموعة"
|
||||
},
|
||||
"settingGroupChat": {
|
||||
"allowDM": {
|
||||
@@ -433,7 +442,7 @@
|
||||
"title": "السماح للمساعد بإرسال رسائل خاصة"
|
||||
},
|
||||
"enableSupervisor": {
|
||||
"desc": "تفعيل وظيفة المشرف لفريق الوكلاء، حيث يدير المشرف سير المحادثة داخل الفريق",
|
||||
"desc": "تفعيل ميزة المشرف على المجموعة، حيث يتولى المشرف إدارة سير المحادثات داخل الفريق",
|
||||
"title": "تفعيل المشرف"
|
||||
},
|
||||
"maxResponseInRow": {
|
||||
@@ -663,7 +672,8 @@
|
||||
"identifier": "معرف المساعد (identifier)",
|
||||
"metaMiss": "يرجى استكمال معلومات المساعد قبل التقديم، يجب أن تتضمن الاسم والوصف والعلامة",
|
||||
"placeholder": "الرجاء إدخال معرف المساعد، يجب أن يكون فريدًا، مثل تطوير الويب",
|
||||
"tooltips": "مشاركة في سوق المساعدين"
|
||||
"success": "تم إرسال المساعد بنجاح",
|
||||
"tooltips": "مشاركة في مجتمع المساعد"
|
||||
},
|
||||
"submitFooter": {
|
||||
"reset": "إعادة تعيين",
|
||||
@@ -758,19 +768,25 @@
|
||||
"tab": {
|
||||
"about": "حول",
|
||||
"agent": "المساعد الافتراضي",
|
||||
"common": "إعدادات عامة",
|
||||
"apikey": "إدارة مفتاح API",
|
||||
"common": "المظهر",
|
||||
"experiment": "تجربة",
|
||||
"hotkey": "اختصارات لوحة المفاتيح",
|
||||
"image": "الرسم بالذكاء الاصطناعي",
|
||||
"image": "خدمة الرسم",
|
||||
"llm": "نموذج اللغة",
|
||||
"profile": "حسابي",
|
||||
"provider": "مزود خدمة الذكاء الاصطناعي",
|
||||
"proxy": "وكيل الشبكة",
|
||||
"security": "الأمان",
|
||||
"stats": "إحصائيات البيانات",
|
||||
"storage": "تخزين البيانات",
|
||||
"sync": "مزامنة السحابة",
|
||||
"system-agent": "مساعد النظام",
|
||||
"tts": "خدمة الكلام"
|
||||
"tts": "خدمة الكلام",
|
||||
"usage": "إحصائيات الاستخدام"
|
||||
},
|
||||
"tools": {
|
||||
"add": "إضافة مكون إضافي",
|
||||
"builtins": {
|
||||
"groupName": "الامتدادات المدمجة"
|
||||
},
|
||||
@@ -796,6 +812,8 @@
|
||||
"tools": "أدوات",
|
||||
"verifyAuth": "لقد أكملت المصادقة"
|
||||
},
|
||||
"notInstalled": "غير مثبت",
|
||||
"notInstalledWarning": "المكون الإضافي الحالي غير مثبت، وقد يؤثر ذلك على استخدام المساعد",
|
||||
"plugins": {
|
||||
"enabled": "ممكّنة {{num}}",
|
||||
"groupName": "الإضافات",
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
{
|
||||
"actions": {
|
||||
"addNewTopic": "إنشاء موضوع جديد",
|
||||
"autoRename": "إعادة تسمية ذكية",
|
||||
"confirmRemoveAll": "سيتم حذف جميع المواضيع، ولن يمكن استعادتها بعد الحذف، يرجى توخي الحذر.",
|
||||
"confirmRemoveTopic": "سيتم حذف هذا الموضوع، ولن يمكن استعادته بعد الحذف، يرجى توخي الحذر.",
|
||||
"confirmRemoveUnstarred": "سيتم حذف المواضيع غير المفضلة، ولن يمكن استعادتها بعد الحذف، يرجى توخي الحذر.",
|
||||
"duplicate": "إنشاء نسخة",
|
||||
"export": "تصدير الموضوع",
|
||||
"openInNewWindow": "افتح الصفحة في نافذة جديدة",
|
||||
"openInNewWindow": "افتح في نافذة مستقلة",
|
||||
"removeAll": "حذف جميع المواضيع",
|
||||
"removeUnstarred": "حذف المواضيع غير المفضلة"
|
||||
},
|
||||
"defaultTitle": "موضوع افتراضي",
|
||||
"displayItems": "عرض العناصر",
|
||||
"duplicateLoading": "يتم نسخ الموضوع...",
|
||||
"duplicateSuccess": "تم نسخ الموضوع بنجاح",
|
||||
"favorite": "مفضل",
|
||||
@@ -32,6 +34,7 @@
|
||||
"desc": "انقر على زر الإرسال على اليسار لحفظ المحادثة الحالية كموضوع تاريخي وبدء جولة جديدة من المحادثة",
|
||||
"title": "قائمة المواضيع"
|
||||
},
|
||||
"loadMore": "المزيد",
|
||||
"searchPlaceholder": "ابحث عن موضوع...",
|
||||
"searchResultEmpty": "لا توجد نتائج للبحث",
|
||||
"temp": "مؤقت",
|
||||
|
||||
+163
-117
@@ -1,339 +1,344 @@
|
||||
{
|
||||
"guide": {
|
||||
"agents": {
|
||||
"replaceBtn": "تغيير",
|
||||
"title": "إضافة توصيات المساعدين:"
|
||||
"replaceBtn": "تبديل مجموعة",
|
||||
"title": "مساعدون جدد مقترحون:"
|
||||
},
|
||||
"defaultMessage": "أنا مساعدك الذكي الشخصي {{appName}}، كيف يمكنني مساعدتك الآن؟<br />إذا كنت بحاجة إلى مساعد أكثر احترافية أو تخصيصًا، يمكنك النقر على <plus /> لإنشاء مساعد مخصص",
|
||||
"defaultMessageWithoutCreate": "أنا مساعدك الذكي الشخصي {{appName}}، كيف يمكنني مساعدتك الآن؟",
|
||||
"defaultMessage": "أنا مساعدك الذكي الشخصي {{appName}}، كيف يمكنني مساعدتك اليوم؟<br />إذا كنت بحاجة إلى مساعد أكثر تخصصًا أو مخصصًا، يمكنك النقر على <plus /> لإنشاء مساعد مخصص",
|
||||
"defaultMessageWithoutCreate": "أنا مساعدك الذكي الشخصي {{appName}}، كيف يمكنني مساعدتك اليوم؟",
|
||||
"groupActivities": {
|
||||
"analysis": {
|
||||
"codeReview": {
|
||||
"description": "مناقشة تقنية ومراجعة الأقران لتغييرات وتنفيذ الشيفرة البرمجية",
|
||||
"description": "مناقشة تقنية ومراجعة جماعية لتغييرات وتنفيذات الشيفرة",
|
||||
"emoji": "💻",
|
||||
"prompt": "دعنا نراجع بعض الشيفرات معًا. هل يمكنك مساعدتنا في تحليل هذه الشيفرات وتحديد مجالات التحسين؟",
|
||||
"prompt": "دعنا نراجع بعض الشيفرات معًا. هل يمكنك مساعدتنا في تحليلها وتحديد مجالات التحسين؟",
|
||||
"title": "مراجعة الشيفرة"
|
||||
},
|
||||
"investment": {
|
||||
"description": "تحليل السوق، مناقشة استراتيجيات الاستثمار ومشاركة الرؤى المالية",
|
||||
"emoji": "📈",
|
||||
"prompt": "دعنا نحلل السوق معًا. هل يمكنك مساعدتنا في مناقشة استراتيجيات الاستثمار ومشاركة الرؤى المالية؟",
|
||||
"prompt": "دعنا نحلل السوق معًا. هل يمكنك مساعدتنا في مناقشة الاستراتيجيات ومشاركة الرؤى؟",
|
||||
"title": "نادي الاستثمار"
|
||||
},
|
||||
"research": {
|
||||
"description": "استكشاف المفاهيم العلمية، إجراء التجارب ومشاركة الاكتشافات",
|
||||
"emoji": "🔬",
|
||||
"prompt": "دعنا نستكشف العلوم معًا! هل يمكنك مساعدتنا في إجراء التجارب ومشاركة اكتشافاتنا؟",
|
||||
"prompt": "دعنا نستكشف العلوم معًا! هل يمكنك مساعدتنا في إجراء التجارب ومشاركة النتائج؟",
|
||||
"title": "معرض العلوم"
|
||||
},
|
||||
"study": {
|
||||
"description": "اجتماعات تعلم تعاونية، مناقشة المفاهيم وحل المشكلات معًا",
|
||||
"description": "جلسات دراسة تعاونية لمناقشة المفاهيم وحل المشكلات معًا",
|
||||
"emoji": "📚",
|
||||
"prompt": "دعنا نشكل مجموعة دراسة. هل يمكنك مساعدتنا في فهم هذه المفاهيم وحل المشكلات معًا؟",
|
||||
"title": "مجموعة الدراسة"
|
||||
"prompt": "دعنا نشكل مجموعة دراسة. هل يمكنك مساعدتنا في فهم المفاهيم وحل المشكلات؟",
|
||||
"title": "مجموعة دراسة"
|
||||
}
|
||||
},
|
||||
"brainstorm": {
|
||||
"artWorkshop": {
|
||||
"description": "إنشاء، نقد وتقدير أشكال مختلفة من الفن البصري والرقمي",
|
||||
"description": "إنشاء، نقد وتقدير الفنون البصرية والرقمية بمختلف أشكالها",
|
||||
"emoji": "🖼️",
|
||||
"prompt": "دعنا نقيم ورشة عمل فنية! هل يمكنك مساعدتنا في إنشاء، نقد وتقدير أشكال مختلفة من الفن؟",
|
||||
"title": "ورشة العمل الفنية"
|
||||
"prompt": "دعنا نقيم ورشة فنية! هل يمكنك مساعدتنا في الإبداع والنقد وتقدير الفنون؟",
|
||||
"title": "ورشة فنية"
|
||||
},
|
||||
"debate": {
|
||||
"description": "نقاشات ومناظرات منظمة حول مواضيع وقضايا مختلفة",
|
||||
"description": "نقاشات منظمة وجدلية حول مواضيع مختلفة وقضايا راهنة",
|
||||
"emoji": "⚖️",
|
||||
"prompt": "دعنا نجري مناظرة منظمة. هل يمكنك مساعدتنا في تنظيم نقاش منطقي حول هذا الموضوع؟",
|
||||
"prompt": "دعنا نقيم مناظرة منظمة. هل يمكنك مساعدتنا في تنظيم نقاش منطقي حول هذا الموضوع؟",
|
||||
"title": "نادي المناظرة"
|
||||
},
|
||||
"designReview": {
|
||||
"description": "اجتماعات تعاونية لتقديم ملاحظات على مفاهيم التصميم، النماذج الأولية أو الأعمال الإبداعية",
|
||||
"description": "جلسات تعاونية لتقديم الملاحظات على المفاهيم والنماذج الأولية والأعمال الإبداعية",
|
||||
"emoji": "🎨",
|
||||
"prompt": "نحتاج إلى مراجعة بعض التصاميم. هل يمكنك مساعدتنا في تقديم ملاحظات بناءة على مفاهيم التصميم والنماذج الأولية؟",
|
||||
"prompt": "نحتاج إلى مراجعة بعض التصاميم. هل يمكنك مساعدتنا في تقديم ملاحظات بناءة؟",
|
||||
"title": "مراجعة التصميم"
|
||||
},
|
||||
"ideation": {
|
||||
"description": "توليد أفكار إبداعية وحل المشكلات بشكل تعاوني من وجهات نظر متعددة",
|
||||
"description": "توليد أفكار إبداعية وحلول مبتكرة من خلال التعاون متعدد الزوايا",
|
||||
"emoji": "🧠",
|
||||
"prompt": "دعنا نبدأ جلسة عصف ذهني للمشروع. هل يمكنك مساعدتنا في توليد أفكار وحلول إبداعية؟",
|
||||
"title": "جلسة العصف الذهني"
|
||||
"prompt": "دعنا نبدأ جلسة عصف ذهني للمشروع. هل يمكنك مساعدتنا في توليد الأفكار والحلول؟",
|
||||
"title": "عصف ذهني"
|
||||
}
|
||||
},
|
||||
"game": {
|
||||
"debateClub": {
|
||||
"description": "نقاشات ومناظرات منظمة حول مواضيع وقضايا مختلفة",
|
||||
"description": "نقاشات منظمة وجدلية حول مواضيع مختلفة وقضايا راهنة",
|
||||
"emoji": "⚖️",
|
||||
"prompt": "دعنا نجري مناظرة منظمة. هل يمكنك مساعدتنا في تنظيم نقاش منطقي حول هذا الموضوع؟",
|
||||
"prompt": "دعنا نقيم مناظرة منظمة. هل يمكنك مساعدتنا في تنظيم نقاش منطقي حول هذا الموضوع؟",
|
||||
"title": "نادي المناظرة"
|
||||
},
|
||||
"gameNight": {
|
||||
"description": "ألعاب تفاعلية ممتعة وأنشطة لبناء الروابط بين الفريق والاستمتاع",
|
||||
"description": "ألعاب وأنشطة تفاعلية ممتعة لبناء روح الفريق والاستمتاع",
|
||||
"emoji": "🎲",
|
||||
"prompt": "ليلة الألعاب بدأت! هل يمكنك مساعدتنا في تنظيم بعض الألعاب التفاعلية الممتعة لبناء الروابط بين الفريق؟",
|
||||
"prompt": "حان وقت ليلة الألعاب! هل يمكنك مساعدتنا في تنظيم ألعاب ممتعة لبناء الفريق؟",
|
||||
"title": "ليلة الألعاب"
|
||||
},
|
||||
"modelUN": {
|
||||
"description": "محاكاة مناظرات الأمم المتحدة والمفاوضات الدبلوماسية حول القضايا العالمية",
|
||||
"emoji": "🌍",
|
||||
"prompt": "دعنا نحاكي مناظرة الأمم المتحدة. هل يمكنك مساعدتنا في إعداد مفاوضات دبلوماسية حول القضايا العالمية؟",
|
||||
"title": "محاكاة الأمم المتحدة"
|
||||
"prompt": "دعنا نحاكي مناظرة في الأمم المتحدة. هل يمكنك مساعدتنا في إعداد مفاوضات دبلوماسية؟",
|
||||
"title": "نموذج الأمم المتحدة"
|
||||
},
|
||||
"werewolf": {
|
||||
"description": "لعبة اجتماعية تعتمد على الاستراتيجية والنقاش لكشف دور الذئب بين اللاعبين",
|
||||
"description": "لعبة استنتاج اجتماعي حيث يحاول اللاعبون كشف المستذئبين من خلال النقاش والاستراتيجية",
|
||||
"emoji": "🐺",
|
||||
"prompt": "دعنا نلعب لعبة الذئب! هل يمكنك مساعدتنا في إعداد القواعد وإدارة هذه اللعبة الاجتماعية الاستنتاجية؟",
|
||||
"title": "لعبة الذئب"
|
||||
"prompt": "دعنا نلعب لعبة المستذئب! هل يمكنك مساعدتنا في إعداد القواعد وإدارة اللعبة؟",
|
||||
"title": "لعبة المستذئب"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"brainstorm": {
|
||||
"description": "توليد أفكار إبداعية وحل المشكلات بشكل تعاوني من وجهات نظر متعددة",
|
||||
"description": "توليد أفكار إبداعية وحلول مبتكرة من خلال التعاون متعدد الزوايا",
|
||||
"emoji": "🧠",
|
||||
"prompt": "دعنا نبدأ جلسة عصف ذهني للمشروع. هل يمكنك مساعدتنا في توليد أفكار وحلول إبداعية؟",
|
||||
"title": "جلسة العصف الذهني"
|
||||
"prompt": "دعنا نبدأ جلسة عصف ذهني للمشروع. هل يمكنك مساعدتنا في توليد الأفكار والحلول؟",
|
||||
"title": "عصف ذهني"
|
||||
},
|
||||
"debate": {
|
||||
"description": "نقاشات ومناظرات منظمة حول مواضيع وقضايا مختلفة",
|
||||
"description": "نقاشات منظمة وجدلية حول مواضيع مختلفة وقضايا راهنة",
|
||||
"emoji": "⚖️",
|
||||
"prompt": "دعنا نجري مناظرة منظمة. هل يمكنك مساعدتنا في تنظيم نقاش منطقي حول هذا الموضوع؟",
|
||||
"prompt": "دعنا نقيم مناظرة منظمة. هل يمكنك مساعدتنا في تنظيم نقاش منطقي حول هذا الموضوع؟",
|
||||
"title": "نادي المناظرة"
|
||||
},
|
||||
"languagePractice": {
|
||||
"description": "ممارسة المحادثة وتعلم لغات جديدة مع الناطقين بها",
|
||||
"description": "ممارسة المحادثة وتعلم لغات جديدة مع متحدثين أصليين",
|
||||
"emoji": "🗣️",
|
||||
"prompt": "دعنا نمارس لغة جديدة معًا. هل يمكنك مساعدتنا في تعلم وممارسة التحدث بهذه اللغة؟",
|
||||
"title": "ممارسة اللغة"
|
||||
"prompt": "دعنا نتدرب على لغة جديدة معًا. هل يمكنك مساعدتنا في التعلم والممارسة؟",
|
||||
"title": "تمرين اللغة"
|
||||
},
|
||||
"studyGroup": {
|
||||
"description": "اجتماعات تعلم تعاونية، مناقشة المفاهيم وحل المشكلات معًا",
|
||||
"description": "جلسات دراسة تعاونية لمناقشة المفاهيم وحل المشكلات معًا",
|
||||
"emoji": "📚",
|
||||
"prompt": "دعنا نشكل مجموعة دراسة. هل يمكنك مساعدتنا في فهم هذه المفاهيم وحل المشكلات معًا؟",
|
||||
"title": "مجموعة الدراسة"
|
||||
"prompt": "دعنا نشكل مجموعة دراسة. هل يمكنك مساعدتنا في فهم المفاهيم وحل المشكلات؟",
|
||||
"title": "مجموعة دراسة"
|
||||
}
|
||||
},
|
||||
"planning": {
|
||||
"cookingClass": {
|
||||
"description": "تعلم ومشاركة مهارات الطهي، الوصفات والتقاليد الطهوية",
|
||||
"description": "تعلم ومشاركة مهارات الطبخ والوصفات والتقاليد الغذائية",
|
||||
"emoji": "👨🍳",
|
||||
"prompt": "دعنا نحضر درس طبخ! هل يمكنك مساعدتنا في تعلم وصفات جديدة ومهارات الطهي؟",
|
||||
"prompt": "دعنا نبدأ درس طبخ! هل يمكنك مساعدتنا في تعلم وصفات ومهارات جديدة؟",
|
||||
"title": "صف الطبخ"
|
||||
},
|
||||
"fitnessChallenge": {
|
||||
"description": "تحديد أهداف لياقة جماعية، مشاركة تمارين وتحفيز بعضنا البعض",
|
||||
"description": "تحديد أهداف لياقة جماعية، مشاركة التمارين وتحفيز بعضنا البعض",
|
||||
"emoji": "💪",
|
||||
"prompt": "دعنا نبدأ تحدي اللياقة! هل يمكنك مساعدتنا في تحديد الأهداف وتحفيز بعضنا البعض للحفاظ على الصحة؟",
|
||||
"prompt": "دعنا نبدأ تحدي اللياقة! هل يمكنك مساعدتنا في تحديد الأهداف وتحفيز بعضنا البعض؟",
|
||||
"title": "تحدي اللياقة"
|
||||
},
|
||||
"planningPoker": {
|
||||
"description": "تقنية تقدير مهام المشروع وحجم العمل باستخدام بطاقات التخطيط الرشيق",
|
||||
"description": "تقنية تقدير مرنة باستخدام بطاقات لتقدير مهام المشروع وحجم العمل",
|
||||
"emoji": "🃏",
|
||||
"prompt": "نحن نقوم بلعب البوكر التخطيطي للمشروع. هل يمكنك مساعدتنا في استخدام تقنيات الرشيق لتقدير حجم هذه المهام؟",
|
||||
"title": "بوكر التخطيط"
|
||||
"prompt": "نحن نخطط باستخدام لعبة التخطيط. هل يمكنك مساعدتنا في تقدير المهام باستخدام تقنيات مرنة؟",
|
||||
"title": "تخطيط البوكر"
|
||||
},
|
||||
"travelPlanning": {
|
||||
"description": "تخطيط الرحلات، مشاركة تجارب السفر واكتشاف وجهات جديدة",
|
||||
"description": "تخطيط الرحلات، مشاركة التجارب واكتشاف وجهات جديدة",
|
||||
"emoji": "✈️",
|
||||
"prompt": "دعنا نخطط رحلة معًا! هل يمكنك مساعدتنا في البحث عن الوجهات وتخطيط مسار الرحلة؟",
|
||||
"prompt": "دعنا نخطط لرحلة معًا! هل يمكنك مساعدتنا في البحث عن وجهات وتنظيم الرحلة؟",
|
||||
"title": "تخطيط السفر"
|
||||
}
|
||||
},
|
||||
"product": {
|
||||
"codeReview": {
|
||||
"description": "مناقشة تقنية ومراجعة الأقران لتغييرات وتنفيذ الشيفرة البرمجية",
|
||||
"description": "مناقشة تقنية ومراجعة جماعية لتغييرات وتنفيذات الشيفرة",
|
||||
"emoji": "💻",
|
||||
"prompt": "دعنا نراجع بعض الشيفرات معًا. هل يمكنك مساعدتنا في تحليل هذه الشيفرات وتحديد مجالات التحسين؟",
|
||||
"prompt": "دعنا نراجع بعض الشيفرات معًا. هل يمكنك مساعدتنا في تحليلها وتحديد مجالات التحسين؟",
|
||||
"title": "مراجعة الشيفرة"
|
||||
},
|
||||
"designReview": {
|
||||
"description": "اجتماعات تعاونية لتقديم ملاحظات على مفاهيم التصميم، النماذج الأولية أو الأعمال الإبداعية",
|
||||
"description": "جلسات تعاونية لتقديم الملاحظات على المفاهيم والنماذج الأولية والأعمال الإبداعية",
|
||||
"emoji": "🎨",
|
||||
"prompt": "نحتاج إلى مراجعة بعض التصاميم. هل يمكنك مساعدتنا في تقديم ملاحظات بناءة على مفاهيم التصميم والنماذج الأولية؟",
|
||||
"prompt": "نحتاج إلى مراجعة بعض التصاميم. هل يمكنك مساعدتنا في تقديم ملاحظات بناءة؟",
|
||||
"title": "مراجعة التصميم"
|
||||
},
|
||||
"sprintPlanning": {
|
||||
"description": "تقنية تقدير مهام المشروع وحجم العمل باستخدام بطاقات التخطيط الرشيق",
|
||||
"description": "تقنية تقدير مرنة باستخدام بطاقات لتقدير مهام المشروع وحجم العمل",
|
||||
"emoji": "🃏",
|
||||
"prompt": "نحن نقوم بلعب البوكر التخطيطي للمشروع. هل يمكنك مساعدتنا في استخدام تقنيات الرشيق لتقدير حجم هذه المهام؟",
|
||||
"title": "بوكر التخطيط"
|
||||
"prompt": "نحن نخطط باستخدام لعبة التخطيط. هل يمكنك مساعدتنا في تقدير المهام باستخدام تقنيات مرنة؟",
|
||||
"title": "تخطيط البوكر"
|
||||
},
|
||||
"techExchange": {
|
||||
"description": "مناقشة التقنيات الناشئة، الابتكار واتجاهات الصناعة",
|
||||
"description": "مناقشة التقنيات الناشئة والابتكار واتجاهات الصناعة",
|
||||
"emoji": "🚀",
|
||||
"prompt": "دعنا نجري تبادلًا تقنيًا! هل يمكنك مساعدتنا في مناقشة التقنيات الناشئة واتجاهات الصناعة؟",
|
||||
"prompt": "دعنا نبدأ تبادلًا تقنيًا! هل يمكنك مساعدتنا في مناقشة التقنيات والاتجاهات الجديدة؟",
|
||||
"title": "تبادل تقني"
|
||||
}
|
||||
},
|
||||
"title": "توصيات لاستخدام الدردشة الجماعية",
|
||||
"title": "اقتراحات لاستخدام الدردشة الجماعية",
|
||||
"writing": {
|
||||
"bookClub": {
|
||||
"description": "مناقشات وتحليلات أدبية للكتب، القصص والأعمال الأدبية",
|
||||
"description": "مناقشة وتحليل الكتب والقصص والأعمال الأدبية",
|
||||
"emoji": "📖",
|
||||
"prompt": "دعنا نبدأ مناقشة نادي الكتاب. هل يمكنك مساعدتنا في تحليل هذا الكتاب ومناقشة مواضيعه؟",
|
||||
"title": "نادي الكتاب"
|
||||
},
|
||||
"movieClub": {
|
||||
"description": "مشاهدة ومناقشة الأفلام، الوثائقيات والوسائط البصرية معًا",
|
||||
"description": "مشاهدة ومناقشة الأفلام والوثائقيات والوسائط البصرية معًا",
|
||||
"emoji": "🎬",
|
||||
"prompt": "دعنا نبدأ مناقشة نادي الأفلام. هل يمكنك مساعدتنا في تحليل هذا الفيلم ومناقشة مواضيعه؟",
|
||||
"title": "نادي الأفلام"
|
||||
"prompt": "دعنا نبدأ مناقشة نادي السينما. هل يمكنك مساعدتنا في تحليل هذا الفيلم ومناقشة مواضيعه؟",
|
||||
"title": "نادي السينما"
|
||||
},
|
||||
"musicSession": {
|
||||
"description": "جلسات تعاون في تأليف الموسيقى، المشاركة والتقدير",
|
||||
"description": "جلسات تعاونية لإنشاء ومشاركة وتقدير الموسيقى",
|
||||
"emoji": "🎵",
|
||||
"prompt": "دعنا نقم بجلسة ارتجال موسيقية! هل يمكنك مساعدتنا في إنشاء وتقدير الموسيقى معًا؟",
|
||||
"title": "جلسة الموسيقى الارتجالية"
|
||||
"prompt": "دعنا نقيم جلسة موسيقية! هل يمكنك مساعدتنا في الإبداع والاستمتاع بالموسيقى؟",
|
||||
"title": "جلسة موسيقية"
|
||||
},
|
||||
"studyGroup": {
|
||||
"description": "اجتماعات تعلم تعاونية، مناقشة المفاهيم وحل المشكلات معًا",
|
||||
"description": "جلسات دراسة تعاونية لمناقشة المفاهيم وحل المشكلات معًا",
|
||||
"emoji": "📚",
|
||||
"prompt": "دعنا نشكل مجموعة دراسة. هل يمكنك مساعدتنا في فهم هذه المفاهيم وحل المشكلات معًا؟",
|
||||
"title": "مجموعة الدراسة"
|
||||
"prompt": "دعنا نشكل مجموعة دراسة. هل يمكنك مساعدتنا في فهم المفاهيم وحل المشكلات؟",
|
||||
"title": "مجموعة دراسة"
|
||||
}
|
||||
}
|
||||
},
|
||||
"groupMessage": "مرحبًا بك في الدردشة الجماعية! تعاون مع عدة مساعدين من الذكاء الاصطناعي في مساحة محادثة مشتركة.",
|
||||
"groupMessage": "مرحبًا بك في الدردشة الجماعية! تعاون مع عدة مساعدين ذكيين في مساحة محادثة مشتركة.",
|
||||
"groupTemplates": {
|
||||
"analysis": {
|
||||
"description": "رؤى مدفوعة بالبيانات، بحث وتحليل معمق",
|
||||
"description": "رؤى مستندة إلى البيانات وتحليلات معمقة",
|
||||
"members": [
|
||||
{
|
||||
"avatar": "📊",
|
||||
"backgroundColor": "#E8F8F5",
|
||||
"plugins": ["steam"],
|
||||
"systemRole": "أنت بارع في معالجة البيانات وتفسيرها، وتكشف عن الأنماط والاتجاهات الكامنة وراء البيانات من خلال الرسوم البيانية والتحليلات الإحصائية.",
|
||||
"systemRole": "أنت بارع في معالجة البيانات وتفسيرها، وتكشف عن الأنماط والاتجاهات من خلال الرسوم البيانية والتحليلات الإحصائية.",
|
||||
"title": "محلل بيانات"
|
||||
},
|
||||
{
|
||||
"avatar": "🧑🔬",
|
||||
"backgroundColor": "#E8F5FF",
|
||||
"systemRole": "أنت خبير بحث، مسؤول عن جمع المعلومات والبحث المعمق، قادر على تحليل المشكلات من عدة أبعاد بشكل شامل.",
|
||||
"title": "خبير بحث"
|
||||
"systemRole": "أنت خبير بحثي، مسؤول عن جمع المعلومات وإجراء دراسات معمقة، وتستطيع تحليل القضايا من عدة أبعاد.",
|
||||
"title": "خبير بحثي"
|
||||
},
|
||||
{
|
||||
"avatar": "📈",
|
||||
"backgroundColor": "#FFF7E8",
|
||||
"systemRole": "أنت خبير إحصاء، متمكن من مختلف الطرق والنماذج الإحصائية، قادر على استخراج رؤى تجارية قيمة من البيانات.",
|
||||
"title": "خبير إحصاء"
|
||||
"systemRole": "أنت خبير إحصائي، تتقن مختلف الأساليب والنماذج الإحصائية، وتستخلص رؤى تجارية قيّمة من البيانات.",
|
||||
"title": "خبير إحصائي"
|
||||
},
|
||||
{
|
||||
"avatar": "🧮",
|
||||
"backgroundColor": "#F0F8FF",
|
||||
"systemRole": "أنت محلل كمي، متخصص في النمذجة الكمية وتقييم المخاطر، تستخدم الطرق الرياضية لحل المشكلات المعقدة.",
|
||||
"systemRole": "أنت محلل كمي، متخصص في النمذجة الكمية وتقييم المخاطر، وتستخدم الأساليب الرياضية لحل المشكلات المعقدة.",
|
||||
"title": "محلل كمي"
|
||||
}
|
||||
],
|
||||
"title": "فريق التحليل"
|
||||
},
|
||||
"brainstorm": {
|
||||
"description": "تفكير إبداعي متعدد الأبعاد، تحفيز إمكانيات لا محدودة",
|
||||
"description": "تفكير إبداعي متعدد الزوايا لإطلاق إمكانيات لا محدودة",
|
||||
"members": [
|
||||
{
|
||||
"avatar": "🧠",
|
||||
"backgroundColor": "#E8F5FF",
|
||||
"systemRole": "أنت مدير إبداعي، ماهر في التحكم في اتجاه الإبداع من منظور شامل، قادر على تحويل المفاهيم المجردة إلى خطط إبداعية قابلة للتنفيذ.",
|
||||
"title": "مدير إبداعي"
|
||||
"systemRole": "أنت مدير إبداعي، بارع في توجيه الرؤية الإبداعية من منظور شامل، وتحويل المفاهيم المجردة إلى أفكار قابلة للتنفيذ.",
|
||||
"title": "المدير الإبداعي"
|
||||
},
|
||||
{
|
||||
"avatar": "🧑🔬",
|
||||
"backgroundColor": "#FFF7E8",
|
||||
"systemRole": "أنت خبير ابتكار، مسؤول عن اكتشاف حلول جديدة وأفكار مبتكرة، تجيد التفكير خارج الأطر التقليدية.",
|
||||
"systemRole": "أنت خبير ابتكار، مسؤول عن اكتشاف حلول جديدة وأفكار خارجة عن المألوف، وتجيد التفكير خارج الصندوق.",
|
||||
"title": "خبير ابتكار"
|
||||
},
|
||||
{
|
||||
"avatar": "🎨",
|
||||
"backgroundColor": "#F6E8FF",
|
||||
"systemRole": "أنت خبير تفكير تصميمي، تفكر من منظور تجربة المستخدم والعرض البصري، تركز على التعبير الإبداعي المرئي.",
|
||||
"title": "خبير تفكير تصميمي"
|
||||
"systemRole": "أنت خبير في التفكير التصميمي، تنظر إلى المشكلات من زاوية تجربة المستخدم والعرض البصري، وتركز على التعبير الإبداعي المرئي.",
|
||||
"title": "خبير التفكير التصميمي"
|
||||
}
|
||||
],
|
||||
"title": "مجموعة العصف الذهني"
|
||||
"title": "فريق العصف الذهني"
|
||||
},
|
||||
"game": {
|
||||
"description": "الاستمتاع بألعاب نصية متعددة اللاعبين مثل لعبة الذئب ومن هو العميل السري",
|
||||
"description": "استمتع بألعاب نصية جماعية مثل لعبة المستذئبين ومن هو الجاسوس",
|
||||
"members": [
|
||||
null,
|
||||
{
|
||||
"avatar": "🧠",
|
||||
"backgroundColor": "#E8F5FF",
|
||||
"systemRole": "أنت مضيف ألعاب، بارع في تنظيم الألعاب النصية الجماعية وتوجيه اللاعبين خلال اللعبة.",
|
||||
"title": "مضيف اللعبة"
|
||||
},
|
||||
{
|
||||
"avatar": "🧑🔬",
|
||||
"backgroundColor": "#FFF7E8",
|
||||
"systemRole": "أنت ماهر في المشاركة في مختلف ألعاب النص المتعددة اللاعبين، وقادر على اللعب وفقًا لقواعد اللعبة.",
|
||||
"systemRole": "أنت بارع في المشاركة في الألعاب النصية الجماعية، وتلعب وفقًا لقواعد اللعبة.",
|
||||
"title": "لاعب"
|
||||
},
|
||||
{
|
||||
"avatar": "🎨",
|
||||
"backgroundColor": "#F6E8FF",
|
||||
"systemRole": "أنت ماهر في المشاركة في مختلف ألعاب النص المتعددة اللاعبين، وقادر على اللعب وفقًا لقواعد اللعبة.",
|
||||
"systemRole": "أنت بارع في المشاركة في الألعاب النصية الجماعية، وتلعب وفقًا لقواعد اللعبة.",
|
||||
"title": "لاعب"
|
||||
},
|
||||
{
|
||||
"avatar": "🧑🎨",
|
||||
"backgroundColor": "#F6E8FF",
|
||||
"systemRole": "أنت ماهر في المشاركة في مختلف ألعاب النص المتعددة اللاعبين، وقادر على اللعب وفقًا لقواعد اللعبة.",
|
||||
"systemRole": "أنت بارع في المشاركة في الألعاب النصية الجماعية، وتلعب وفقًا لقواعد اللعبة.",
|
||||
"title": "لاعب"
|
||||
}
|
||||
],
|
||||
"title": "صالة الألعاب"
|
||||
"title": "قاعة الألعاب"
|
||||
},
|
||||
"planning": {
|
||||
"description": "التخطيط الاستراتيجي وإدارة المشاريع، تنسيق شامل",
|
||||
"description": "تخطيط استراتيجي وإدارة مشاريع شاملة",
|
||||
"members": [
|
||||
{
|
||||
"avatar": "📋",
|
||||
"backgroundColor": "#E8F5FF",
|
||||
"systemRole": "أنت مسؤول عن التخطيط العام للمشروع، مراقبة التقدم وتنسيق الموارد لضمان إتمام المشروع في الوقت المحدد وبجودة عالية.",
|
||||
"title": "طباخ"
|
||||
"systemRole": "أنت مسؤول عن التخطيط العام للمشروع، وضبط الجدول الزمني، وتنسيق الموارد لضمان إنجاز المشروع بجودة عالية وفي الوقت المحدد.",
|
||||
"title": "الطاهي"
|
||||
},
|
||||
{
|
||||
"avatar": "🎯",
|
||||
"backgroundColor": "#FFF7E8",
|
||||
"systemRole": "أنت مسؤول عن وضع الخطط الاستراتيجية طويلة الأمد، تحليل فرص السوق، تحديد الأهداف ومسارات التنفيذ.",
|
||||
"title": "خبير شراء المواد"
|
||||
"systemRole": "أنت مسؤول عن وضع الخطط الاستراتيجية طويلة المدى، وتحليل الفرص السوقية، وتحديد الأهداف ومسارات التنفيذ.",
|
||||
"title": "خبير شراء المكونات"
|
||||
},
|
||||
{
|
||||
"avatar": "🧑🎨",
|
||||
"backgroundColor": "#F0F8FF",
|
||||
"systemRole": "أنت مسؤول عن وضع خطط تنفيذية مفصلة، تنسيق موارد الأقسام المختلفة وضمان قابلية تنفيذ الخطة.",
|
||||
"title": "خبير تطوير الطعام"
|
||||
"systemRole": "أنت مسؤول عن إعداد خطط تنفيذية مفصلة، وتنسيق الموارد بين الأقسام المختلفة لضمان قابلية تنفيذ الخطة.",
|
||||
"title": "خبير تطوير الأطعمة"
|
||||
}
|
||||
],
|
||||
"title": "فريق تطوير الطعام"
|
||||
"title": "فريق تطوير الأطعمة"
|
||||
},
|
||||
"product": {
|
||||
"description": "تصميم وتطوير المنتجات، ابتكار منتجات عالية الجودة",
|
||||
"description": "تصميم وتطوير المنتجات لإنشاء منتجات عالية الجودة",
|
||||
"members": [
|
||||
{
|
||||
"avatar": "🎨",
|
||||
"backgroundColor": "#F6E8FF",
|
||||
"systemRole": "أنت مصمم، ماهر في تصميم مختلف أنواع المنتجات، قادر على التصميم وفق متطلبات المنتج.",
|
||||
"systemRole": "أنت مصمم، بارع في تصميم أنواع مختلفة من المنتجات، وتعمل وفقًا لمتطلبات المنتج.",
|
||||
"title": "مصمم"
|
||||
},
|
||||
{
|
||||
"avatar": "🧑",
|
||||
"backgroundColor": "#E8F5FF",
|
||||
"systemRole": "أنت مدير منتج، مسؤول عن تخطيط وتصميم وتطوير وصيانة المنتج، لضمان جودة المنتج وتجربة المستخدم.",
|
||||
"systemRole": "أنت مدير منتج، مسؤول عن تخطيط وتصميم وتطوير وصيانة المنتج، وتضمن جودة المنتج وتجربة المستخدم.",
|
||||
"title": "مدير منتج"
|
||||
},
|
||||
{
|
||||
"avatar": "🧑💻",
|
||||
"backgroundColor": "#E8F8F5",
|
||||
"systemRole": "أنت مهندس شامل ذو خبرة، ماهر في تطوير مختلف أنواع المنتجات، قادر على التطوير وفق متطلبات المنتج.",
|
||||
"title": "مهندس شامل"
|
||||
"systemRole": "أنت مهندس برمجيات شامل ذو خبرة، بارع في تطوير أنواع مختلفة من المنتجات، وتعمل وفقًا لمتطلبات المنتج.",
|
||||
"title": "مهندس برمجيات شامل"
|
||||
}
|
||||
],
|
||||
"title": "فريق تطوير المنتج"
|
||||
"title": "فريق تطوير المنتجات"
|
||||
},
|
||||
"writing": {
|
||||
"description": "إنشاء المحتوى والتحرير، ابتكار نصوص عالية الجودة",
|
||||
"description": "إنشاء وتحرير المحتوى لصياغة نصوص عالية الجودة",
|
||||
"members": [
|
||||
{
|
||||
"avatar": "✍️",
|
||||
"backgroundColor": "#F6E8FF",
|
||||
"systemRole": "أنت ماهر في إنشاء محتوى بأنماط أدبية مختلفة، وقادر على تعديل أسلوب الكتابة حسب المشاهد والجمهور.",
|
||||
"systemRole": "أنت بارع في كتابة أنواع مختلفة من المحتوى، وتستطيع تعديل أسلوب الكتابة حسب السياق والجمهور المستهدف.",
|
||||
"title": "كاتب محتوى"
|
||||
},
|
||||
{
|
||||
"avatar": "🧑🎨",
|
||||
"backgroundColor": "#E8F8F5",
|
||||
"systemRole": "أنت محرر، مسؤول عن تدقيق النصوص، تنقيحها وتحسينها، لضمان دقة المحتوى وسلاسته واحترافيته.",
|
||||
"systemRole": "أنت محرر، مسؤول عن تدقيق النصوص وتحريرها وتحسينها، لضمان دقة المحتوى وسلاسته واحترافيته.",
|
||||
"title": "محرر"
|
||||
}
|
||||
],
|
||||
@@ -341,22 +346,63 @@
|
||||
}
|
||||
},
|
||||
"questions": {
|
||||
"moreBtn": "معرفة المزيد",
|
||||
"title": "جرّب أن تسأل:"
|
||||
"moreBtn": "اعرف المزيد",
|
||||
"title": "جرب أن تسأل:"
|
||||
},
|
||||
"welcome": {
|
||||
"afternoon": "مساء الخير",
|
||||
"morning": "صباح الخير",
|
||||
"night": "مساء الخير",
|
||||
"noon": "نهاراً"
|
||||
"noon": "نهارك سعيد"
|
||||
}
|
||||
},
|
||||
"header": "مرحبًا بكم في الاستخدام",
|
||||
"pickAgent": "أو اختيار قالب مساعد من القائمة التالية",
|
||||
"skip": "تخطى الإنشاء",
|
||||
"header": "مرحبًا بك",
|
||||
"pickAgent": "أو اختر من قوالب المساعدين التالية",
|
||||
"skip": "تخطي الإنشاء",
|
||||
"slogan": {
|
||||
"desc1": "قم بتشغيل عقلك الجماعي وأشعل شرارة التفكير. مساعدك الذكي، دائمًا موجود.",
|
||||
"desc2": "أنشئ مساعدك الأول ولنبدأ!",
|
||||
"title": "امنح نفسك عقلاً أذكى"
|
||||
"desc1": "فعّل طاقة العقول، وأطلق شرارة الإبداع. مساعدك الذكي دائمًا هنا.",
|
||||
"desc2": "أنشئ أول مساعد لك، ولنبدأ الرحلة ~",
|
||||
"title": "امنح نفسك عقلًا أكثر ذكاءً"
|
||||
},
|
||||
"welcomeMessages": {
|
||||
"1": "مرحبًا بعودتك 😊",
|
||||
"10": "أقصى إنتاجية الآن~",
|
||||
"11": "في خدمتك!",
|
||||
"12": "شكرًا لانتظارك ☕",
|
||||
"13": "لنبدأ الآن ✅",
|
||||
"14": "هل لديك سؤال جديد؟",
|
||||
"15": "عمل رائع اليوم!",
|
||||
"16": "جاري تحميل الإلهام",
|
||||
"17": "متصل بكامل الطاقة ⚡",
|
||||
"18": "انطلاق! 🚀",
|
||||
"19": "أفكاري تواكبك الآن.",
|
||||
"2": "مرحبًا، أنا هنا",
|
||||
"20": "الإلهام قادم",
|
||||
"21": "بانتظار إشارتك",
|
||||
"22": "وضع الإنتاجية مفعل!",
|
||||
"23": "في وضع الاستعداد",
|
||||
"24": "جاهز للتحدي",
|
||||
"25": "أفكار جديدة قيد التكوين",
|
||||
"26": "الطريق واضح، لننطلق!",
|
||||
"27": "النظام جاهز لمساعدتك 💡",
|
||||
"28": "جاري تحميل مزاج جيد",
|
||||
"29": "تحكم بالإيقاع من الآن 🎵",
|
||||
"3": "أنا جاهز!",
|
||||
"30": "رفع الكفاءة …",
|
||||
"31": "هدف اليوم قيد الإنجاز 🎯",
|
||||
"32": "دع الإلهام يتألق ✨",
|
||||
"33": "تم تحديث المهام",
|
||||
"34": "كل شيء جاهز",
|
||||
"35": "وضع السرعة مفعل",
|
||||
"36": "هيا نبدأ 😎",
|
||||
"37": "أنا هنا بانتظارك",
|
||||
"38": "استمر في الأداء الرائع!",
|
||||
"39": "لا تنسَ أن تأخذ قسطًا من الراحة~ 💤",
|
||||
"4": "سعيد برؤيتك",
|
||||
"5": "هل أنت مستعد للبدء؟",
|
||||
"6": "دعني أساعدك اليوم",
|
||||
"7": "لنواصل التقدم!",
|
||||
"8": "لننجزها معًا 💪",
|
||||
"9": "لنبدأ العمل 🏃♂️"
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -133,7 +133,7 @@
|
||||
"passwordPlaceholder": "Моля, въведете парола",
|
||||
"signinLink": "Влезте сега",
|
||||
"submit": "Регистрация",
|
||||
"subtitle": "Присъединете се към общността на LobeChat",
|
||||
"subtitle": "Стартиране на съвместното пространство на Agents",
|
||||
"success": "Регистрацията е успешна! Моля, проверете имейла си за потвърждение",
|
||||
"title": "Създаване на акаунт",
|
||||
"usernamePlaceholder": "Моля, въведете потребителско име"
|
||||
@@ -141,8 +141,7 @@
|
||||
"verifyEmail": {
|
||||
"backToSignIn": "Обратно към вход",
|
||||
"checkSpam": "Ако не сте получили имейл, моля проверете папката със спам",
|
||||
"descriptionPrefix": "Изпратихме имейл за потвърждение на",
|
||||
"descriptionSuffix": "",
|
||||
"description": "Потвърдителен имейл е изпратен до {{email}}",
|
||||
"resend": {
|
||||
"button": "Изпрати отново имейл за потвърждение",
|
||||
"error": "Изпращането не бе успешно, моля опитайте по-късно",
|
||||
@@ -156,6 +155,11 @@
|
||||
"prevMonth": "Миналия месец",
|
||||
"recent30Days": "Последните 30 дни"
|
||||
},
|
||||
"footer": {
|
||||
"agreement": "Продължавайки, потвърждавате, че сте прочели и се съгласявате с <terms>Условията за ползване</terms> и <privacy>Политиката за поверителност</privacy>",
|
||||
"privacy": "Политика за поверителност",
|
||||
"terms": "Условия за ползване"
|
||||
},
|
||||
"header": {
|
||||
"desc": "Управлявайте информацията за вашия акаунт.",
|
||||
"title": "Акаунт"
|
||||
@@ -221,6 +225,10 @@
|
||||
"usernameRule": "Потребителското име може да съдържа само букви, цифри или долна черта",
|
||||
"usernameUpdateFailed": "Неуспешно актуализиране на потребителското име, моля, опитайте отново по-късно"
|
||||
},
|
||||
"signin": {
|
||||
"subtitle": "Регистрирайте се или влезте във вашия {{appName}} акаунт",
|
||||
"title": "Вашето съвместно пространство на Agents"
|
||||
},
|
||||
"signout": "Изход",
|
||||
"signup": "Регистрация",
|
||||
"stats": {
|
||||
@@ -266,7 +274,7 @@
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Управление на API ключове",
|
||||
"profile": "Профил",
|
||||
"profile": "Моят акаунт",
|
||||
"security": "Сигурност",
|
||||
"stats": "Статистика",
|
||||
"usage": "Статистика за използване"
|
||||
|
||||
+22
-15
@@ -22,13 +22,13 @@
|
||||
},
|
||||
"clearCurrentMessages": "Изчисти съобщенията от текущата сесия",
|
||||
"confirmClearCurrentMessages": "На път си да изчистиш съобщенията от текущата сесия. След като бъдат изчистени, те не могат да бъдат възстановени. Моля, потвърди действието си.",
|
||||
"confirmRemoveChatGroupItemAlert": "Този екип на Agent ще бъде изтрит. Членовете на екипа няма да бъдат засегнати. Моля, потвърдете действието си.",
|
||||
"confirmRemoveChatGroupItemAlert": "Тази група ще бъде изтрита, но членовете на екипа няма да бъдат засегнати. Моля, потвърдете действието си.",
|
||||
"confirmRemoveGroupItemAlert": "Ще изтриете тази група. След изтриването помощниците ѝ ще бъдат преместени в списъка по подразбиране. Моля, потвърдете действието си.",
|
||||
"confirmRemoveGroupSuccess": "Екипът на агентите беше успешно изтрит",
|
||||
"confirmRemoveGroupSuccess": "Групата беше изтрита успешно",
|
||||
"confirmRemoveSessionItemAlert": "На път си да изтриеш този агент. След като бъде изтрит, той не може да бъде възстановен. Моля, потвърди действието си.",
|
||||
"confirmRemoveSessionSuccess": "Сесията е успешно изтрита",
|
||||
"defaultAgent": "Агент по подразбиране",
|
||||
"defaultGroupChat": "Екип на агентите",
|
||||
"defaultGroupChat": "Група",
|
||||
"defaultList": "Списък по подразбиране",
|
||||
"defaultSession": "Агент по подразбиране",
|
||||
"dm": {
|
||||
@@ -44,6 +44,7 @@
|
||||
},
|
||||
"duplicateTitle": "{{title}} Копие",
|
||||
"emptyAgent": "Няма наличен асистент",
|
||||
"emptyAgentAction": "Създаване на асистент",
|
||||
"extendParams": {
|
||||
"disableContextCaching": {
|
||||
"desc": "Разходите за генериране на единичен разговор могат да бъдат намалени с до 90%, а скоростта на отговорите да се увеличи 4 пъти (<1>Научете повече</1>). При активиране автоматично ще се деактивира ограничението на броя на историческите съобщения",
|
||||
@@ -120,7 +121,7 @@
|
||||
"noTemplateMembers": "В шаблона няма членове",
|
||||
"noTemplates": "Няма налични шаблони",
|
||||
"searchTemplates": "Търсене на шаблони...",
|
||||
"title": "Създаване на екип на агентите",
|
||||
"title": "Създаване на група",
|
||||
"useTemplate": "Използвай шаблон"
|
||||
},
|
||||
"hideForYou": "Съдържанието на личните съобщения е скрито. Моля, активирайте „Показване на съдържанието на личните съобщения“ в настройките, за да го видите.",
|
||||
@@ -154,25 +155,25 @@
|
||||
"knowledgeBase": {
|
||||
"all": "Всички съдържания",
|
||||
"allFiles": "Всички файлове",
|
||||
"allKnowledgeBases": "Всички знания",
|
||||
"disabled": "Текущият режим на внедряване не поддържа разговори с база знания. Ако искате да използвате тази функция, моля, превключете на внедряване с база данни на сървъра или използвайте услугата {{cloud}}.",
|
||||
"allLibraries": "Всички ресурси",
|
||||
"disabled": "Текущият режим на внедряване не поддържа диалог с ресурсната база. За да използвате тази функция, моля, преминете към внедряване със сървърна база данни или използвайте услугата {{cloud}}",
|
||||
"library": {
|
||||
"action": {
|
||||
"add": "Добави",
|
||||
"detail": "Детайли",
|
||||
"remove": "Премахни"
|
||||
},
|
||||
"title": "Файлове/База знания"
|
||||
"title": "Файлове/Ресурсна база"
|
||||
},
|
||||
"relativeFilesOrKnowledgeBases": "Свързани файлове/бази знания",
|
||||
"title": "База знания",
|
||||
"uploadGuide": "Качените файлове могат да бъдат прегледани в „База знания“",
|
||||
"relativeFilesOrLibraries": "Свързани файлове/ресурси",
|
||||
"title": "Ресурсна база",
|
||||
"uploadGuide": "Качените файлове могат да бъдат прегледани в раздела „Ресурси“",
|
||||
"viewMore": "Вижте още"
|
||||
},
|
||||
"memberSelection": {
|
||||
"addMember": "Добавяне на член",
|
||||
"allMembers": "Всички членове",
|
||||
"createGroup": "Създаване на екип на Agent",
|
||||
"createGroup": "Създаване на група",
|
||||
"noAvailableAgents": "Няма налични агенти за покана",
|
||||
"noSelectedAgents": "Все още не са избрани агенти",
|
||||
"searchAgents": "Търсене на агент...",
|
||||
@@ -245,9 +246,10 @@
|
||||
"senderAssistant": "Агент",
|
||||
"senderUser": "Ти"
|
||||
},
|
||||
"newAgent": "Нов агент",
|
||||
"newGroupChat": "Нов екип на агентите",
|
||||
"noAgentsYet": "Този екип на агентите все още няма членове. Натиснете бутона +, за да поканите асистент.",
|
||||
"newAgent": "Създаване на асистент",
|
||||
"newGroupChat": "Създаване на групов чат",
|
||||
"newPage": "Създаване на документ",
|
||||
"noAgentsYet": "Тази група все още няма членове. Натиснете бутона +, за да поканите асистент.",
|
||||
"noAvailableAgents": "Няма налични членове за покана",
|
||||
"noMatchingAgents": "Няма съвпадащи членове",
|
||||
"noMembersYet": "В тази група все още няма членове. Щракнете върху бутона +, за да поканите асистенти.",
|
||||
@@ -296,7 +298,7 @@
|
||||
"searchAgentPlaceholder": "Търсач на помощ...",
|
||||
"searchAgents": "Асистент за търсене...",
|
||||
"selectedAgents": "Избрани помощници",
|
||||
"sendPlaceholder": "Напиши съобщението си тук...",
|
||||
"sendPlaceholder": "Задайте въпрос, създайте нещо или започнете задача, <hotkey><hotkey/>",
|
||||
"sessionGroup": {
|
||||
"config": "Управление на групи",
|
||||
"confirmRemoveGroupAlert": "Тази група е на път да бъде изтрита. След изтриването, агентите в тази група ще бъдат преместени в списъка по подразбиране. Моля, потвърди действието си.",
|
||||
@@ -361,6 +363,10 @@
|
||||
"title": "Задачите са изпълнени"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"profile": "Профил на асистента",
|
||||
"search": "Търсене"
|
||||
},
|
||||
"thread": {
|
||||
"divider": "Подтема",
|
||||
"threadMessageCount": "{{messageCount}} съобщения",
|
||||
@@ -413,6 +419,7 @@
|
||||
"checkOpenNewTopic": "Да се отвори ли нова тема?",
|
||||
"checkSaveCurrentMessages": "Искате ли да запазите текущата сесия като тема?",
|
||||
"openNewTopic": "Отвори нова тема",
|
||||
"recent": "Последни теми",
|
||||
"saveCurrentMessages": "Запази текущата сесия като тема"
|
||||
},
|
||||
"translate": {
|
||||
|
||||
@@ -137,6 +137,8 @@
|
||||
"close": "Затвори",
|
||||
"cmdk": {
|
||||
"about": "Относно",
|
||||
"aiModeEmptyState": "Въведете въпроса си в полето по-горе, за да започнете разговор с AI",
|
||||
"aiModePlaceholder": "Задайте въпрос на AI...",
|
||||
"communitySupport": "Общностна поддръжка",
|
||||
"discover": "Открий",
|
||||
"knowledgeBase": "База знания",
|
||||
@@ -145,6 +147,16 @@
|
||||
"noResults": "Няма намерени резултати",
|
||||
"openSettings": "Отвори настройките",
|
||||
"painting": "AI Рисуване",
|
||||
"search": {
|
||||
"agent": "Асистент",
|
||||
"agents": "Асистенти",
|
||||
"file": "Файл",
|
||||
"files": "Файлове",
|
||||
"loading": "Търсене...",
|
||||
"searching": "Резултати от търсенето",
|
||||
"topic": "Тема",
|
||||
"topics": "Теми"
|
||||
},
|
||||
"searchPlaceholder": "Въведете команда или търсене...",
|
||||
"settings": "Настройки",
|
||||
"starOnGitHub": "Дайте ни звезда в GitHub",
|
||||
@@ -304,6 +316,13 @@
|
||||
"business": "Бизнес сътрудничество",
|
||||
"support": "Поддръжка по имейл"
|
||||
},
|
||||
"navPanel": {
|
||||
"agent": "Асистент",
|
||||
"displayItems": "Показване на елементи",
|
||||
"library": "Библиотека",
|
||||
"searchAgent": "Търсене на асистент...",
|
||||
"searchResultEmpty": "Няма намерени резултати"
|
||||
},
|
||||
"new": "Нов",
|
||||
"oauth": "SSO Вход",
|
||||
"officialSite": "Официален сайт",
|
||||
@@ -358,13 +377,21 @@
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"aiImage": "AI рисуване",
|
||||
"aiImage": "Рисуване",
|
||||
"audio": "Аудио",
|
||||
"chat": "Чат",
|
||||
"community": "Общност",
|
||||
"discover": "Открий",
|
||||
"files": "Файлове",
|
||||
"home": "Начало",
|
||||
"knowledgeBase": "База знания",
|
||||
"me": "аз",
|
||||
"setting": "Настройки"
|
||||
"memory": "Памет",
|
||||
"pages": "Документи",
|
||||
"resource": "Ресурси",
|
||||
"search": "Търсене",
|
||||
"setting": "Настройки",
|
||||
"video": "Видео"
|
||||
},
|
||||
"telemetry": {
|
||||
"allow": "Разреши",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"chunkingTooltip": "Разделете файла на множество текстови блокове и ги векторизирайте, за да се използват за семантично търсене и диалог с файла",
|
||||
"chunkingUnsupported": "Този файл не поддържа разделяне на части.",
|
||||
"confirmDelete": "Ще изтриете този файл. След изтриването му няма да може да бъде възстановен. Моля, потвърдете действието си.",
|
||||
"confirmDeleteFolder": "Папката и цялото ѝ съдържание ще бъдат изтрити. След изтриването няма да могат да бъдат възстановени. Моля, потвърдете действието си.",
|
||||
"confirmDeleteMultiFiles": "Ще изтриете избраните {{count}} файла. След изтриването им няма да могат да бъдат възстановени. Моля, потвърдете действието си.",
|
||||
"confirmRemoveFromKnowledgeBase": "Ще премахнете избраните {{count}} файла от базата знания. След премахването им файловете все още могат да бъдат видяни в списъка с всички файлове. Моля, потвърдете действието си.",
|
||||
"copyUrl": "Копирай линк",
|
||||
@@ -26,8 +27,19 @@
|
||||
"createChunkingTask": "Подготовка...",
|
||||
"deleteSuccess": "Файлът е изтрит успешно",
|
||||
"downloading": "Изтегляне на файла...",
|
||||
"goBack": "Назад към предишната страница",
|
||||
"goForward": "Напред към следващата страница",
|
||||
"goToParent": "Влизане в родителската папка",
|
||||
"moveError": "Неуспешно преместване на файла",
|
||||
"moveHere": "Премести тук",
|
||||
"moveSuccess": "Файлът беше преместен успешно",
|
||||
"moveToFolder": "Премести в...",
|
||||
"moveToRoot": "Премести в основната директория",
|
||||
"removeFromKnowledgeBase": "Премахни от базата знания",
|
||||
"removeFromKnowledgeBaseSuccess": "Файлът е премахнат успешно"
|
||||
"removeFromKnowledgeBaseSuccess": "Файлът е премахнат успешно",
|
||||
"rename": "Преименуване",
|
||||
"renameError": "Неуспешно преименуване",
|
||||
"renameSuccess": "Успешно преименуване"
|
||||
},
|
||||
"bottom": "Достигнахте края",
|
||||
"config": {
|
||||
@@ -42,6 +54,12 @@
|
||||
"or": "или",
|
||||
"title": "Плъзнете файл или папка тук"
|
||||
},
|
||||
"noFolders": "Няма налични папки",
|
||||
"sort": {
|
||||
"dateAdded": "Дата на добавяне",
|
||||
"name": "Име",
|
||||
"size": "Размер"
|
||||
},
|
||||
"title": {
|
||||
"createdAt": "Дата на създаване",
|
||||
"size": "Размер",
|
||||
|
||||
@@ -60,9 +60,9 @@
|
||||
},
|
||||
"list": "Списък с асистенти",
|
||||
"marketSource": {
|
||||
"label": "Превключване на източник на пазара",
|
||||
"legacy": "Стар пазар",
|
||||
"new": "Нов пазар"
|
||||
"label": "Превключване на източника на общността",
|
||||
"legacy": "Стара общност",
|
||||
"new": "Нова общност"
|
||||
},
|
||||
"more": "Още",
|
||||
"plugins": "Интегрирани плъгини",
|
||||
@@ -85,7 +85,7 @@
|
||||
"subtitle": "Асистентът, който се опитвате да достъпите, е архивиран поради една от следните възможни причини:",
|
||||
"title": "Асистентът е архивиран"
|
||||
},
|
||||
"backToMarket": "Обратно към пазара на асистенти",
|
||||
"backToMarket": "Обратно към общността на асистентите",
|
||||
"deprecated": {
|
||||
"reasons": {
|
||||
"official": "Асистентът е премахнат от официалните лица поради проблеми със сигурността/политиката",
|
||||
@@ -94,9 +94,9 @@
|
||||
"subtitle": "Асистентът, който се опитвате да достъпите, е отхвърлен поради една от следните възможни причини:",
|
||||
"title": "Асистентът е отхвърлен"
|
||||
},
|
||||
"support": "Ако имате въпроси, моля копирайте линка и го изпратете на <1>support@lobehub.com</1> за съдействие.",
|
||||
"support": "Ако имате въпроси, моля копирайте линка и го изпратете на <email>support@lobehub.com</email> за консултация.",
|
||||
"unpublished": {
|
||||
"subtitle": "Асистентът, който се опитвате да достъпите, е в процес на преглед. Ако имате въпроси, копирайте линка и го изпратете на <1>support@lobehub.com</1> за съдействие.",
|
||||
"subtitle": "Асистентът, който се опитвате да достъпите, в момента преминава през преглед на версията. Ако имате въпроси, копирайте линка и го изпратете на <email>support@lobehub.com</email> за консултация.",
|
||||
"title": "Асистентът е в процес на преглед"
|
||||
}
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
"createGuide": {
|
||||
"func1": {
|
||||
"desc1": "Влез в настройките на асистента, който искаш да добавиш, чрез иконата в горния десен ъгъл на прозореца за разговор;",
|
||||
"desc2": "Натисни бутона за добавяне на асистент в горния десен ъгъл.",
|
||||
"desc2": "Кликнете върху бутона в горния десен ъгъл, за да изпратите към общността на асистентите.",
|
||||
"tag": "Метод 1",
|
||||
"title": "Добавяне чрез LobeChat"
|
||||
},
|
||||
@@ -186,8 +186,10 @@
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"communityAgents": "Общностни асистенти",
|
||||
"featuredAssistants": "Препоръчани асистенти",
|
||||
"featuredModels": "Препоръчани модели",
|
||||
"featuredPlugins": "Препоръчани плъгини",
|
||||
"featuredProviders": "Препоръчани доставчици на модели",
|
||||
"featuredTools": "Препоръчани инструменти",
|
||||
"more": "Открий повече"
|
||||
@@ -516,7 +518,7 @@
|
||||
"hero": {
|
||||
"desc": "Отворена и разгръщаема платформа MCP сървъри, която помага на AI системите лесно да имат достъп до файлови системи, бази данни, API и други ключови ресурси, разширявайки вашите AI възможности.",
|
||||
"subTitle": "Отворен код & готово за използване",
|
||||
"title": "Отворен MCP пазар за AI"
|
||||
"title": "Отворена MCP общност, ориентирана към AI"
|
||||
},
|
||||
"sorts": {
|
||||
"createdAt": "Последно добавено",
|
||||
@@ -529,7 +531,7 @@
|
||||
"toolsCount": "Брой инструменти",
|
||||
"updatedAt": "Последна актуализация"
|
||||
},
|
||||
"title": "MCP пазар",
|
||||
"title": "MCP общност",
|
||||
"unvalidated": {
|
||||
"desc": "Този MCP сървър все още не е проверен",
|
||||
"title": "Непроверен"
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
"on": "Покажи лентата за форматиране"
|
||||
}
|
||||
},
|
||||
"autoSave": {
|
||||
"latest": "Заредена е най-новата версия",
|
||||
"saved": "Запазено",
|
||||
"saving": "Автоматично запазване..."
|
||||
},
|
||||
"cancel": "Отказ",
|
||||
"confirm": "Потвърждение",
|
||||
"file": {
|
||||
@@ -20,10 +25,18 @@
|
||||
},
|
||||
"link": {
|
||||
"edit": "Редактирай връзката",
|
||||
"editLinkTitle": "Връзка",
|
||||
"editTextTitle": "Заглавие",
|
||||
"open": "Отвори връзката",
|
||||
"placeholder": "Въведете URL адрес на връзката",
|
||||
"unlink": "Премахни връзката"
|
||||
},
|
||||
"markdown": {
|
||||
"cancel": "Отказ",
|
||||
"confirm": "Преобразуване",
|
||||
"parseMessage": "Съдържанието ще бъде преобразувано във формат Markdown и съществуващото съдържание ще бъде заменено. Сигурни ли сте? (Автоматично затваряне след 5 секунди)",
|
||||
"parseTitle": "Форматиране в Markdown"
|
||||
},
|
||||
"math": {
|
||||
"placeholder": "Моля, въведете TeX формула"
|
||||
},
|
||||
@@ -50,13 +63,16 @@
|
||||
"bulletList": "Маркиран списък",
|
||||
"code": "Код в реда",
|
||||
"codeblock": "Блок с код",
|
||||
"image": "Изображение",
|
||||
"italic": "Курсив",
|
||||
"link": "Връзка",
|
||||
"numberList": "Номериран списък",
|
||||
"redo": "Повтори",
|
||||
"strikethrough": "Зачеркване",
|
||||
"table": "таблица",
|
||||
"taskList": "Списък със задачи",
|
||||
"tex": "TeX формула",
|
||||
"underline": "Подчертаване"
|
||||
"underline": "Подчертаване",
|
||||
"undo": "Отмени"
|
||||
}
|
||||
}
|
||||
|
||||
+34
-15
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"addFolder": "Създаване на папка",
|
||||
"addKnowledge": "Добавяне на знание",
|
||||
"addLibrary": "Добавяне към библиотеката",
|
||||
"addPage": "Създаване на документ",
|
||||
"desc": "Управлявайте знанията си за работа, учене и живот.",
|
||||
"desc": "Управлявайте своите ресурси за работа, учене и живот.",
|
||||
"detail": {
|
||||
"basic": {
|
||||
"createdAt": "Дата на създаване",
|
||||
@@ -50,6 +50,12 @@
|
||||
"pin": "Закачане на документа"
|
||||
},
|
||||
"saving": "Запазване...",
|
||||
"slashCommands": {
|
||||
"bulletedList": "Маркиран списък",
|
||||
"image": "Изображение",
|
||||
"orderedList": "Номериран списък",
|
||||
"todoList": "Списък със задачи"
|
||||
},
|
||||
"titlePlaceholder": "Без заглавие",
|
||||
"wordCount": "{{wordCount}} думи"
|
||||
},
|
||||
@@ -57,14 +63,27 @@
|
||||
"copyContent": "Копиране на цялото съдържание",
|
||||
"duplicate": "Създаване на копие",
|
||||
"empty": "Все още няма документи. Щракнете върху бутона по-горе, за да създадете първия си документ.",
|
||||
"filter": {
|
||||
"all": "Всички",
|
||||
"onlyInPages": "Само в документите"
|
||||
},
|
||||
"noResults": "Няма намерени съвпадащи документи",
|
||||
"pageCount": "Общо {{count}} документа",
|
||||
"selectNote": "Изберете документ, за да започнете редактиране",
|
||||
"title": "Документи",
|
||||
"untitled": "Без заглавие"
|
||||
},
|
||||
"empty": "Няма качени файлове/папки",
|
||||
"header": {
|
||||
"actions": {
|
||||
"connect": "Свързване...",
|
||||
"gitignore": {
|
||||
"apply": "Приложи правилата",
|
||||
"cancel": "Игнорирай правилата",
|
||||
"content": "Открит е файл .gitignore (общо {{count}} файла). Да се приложат ли правилата за игнориране?",
|
||||
"filtered": "Филтрирани са {{ignored}} файла (от общо {{total}} файла)",
|
||||
"title": "Открит е .gitignore файл"
|
||||
},
|
||||
"newFolder": "Нова папка",
|
||||
"newPage": "Създаване на нов документ",
|
||||
"uploadFile": "Качване на файл",
|
||||
@@ -91,7 +110,7 @@
|
||||
"quickActions": "Бързи действия",
|
||||
"recentFiles": "Скорошни файлове",
|
||||
"recentPages": "Скорошни документи",
|
||||
"subtitle": "Добре дошли в базата знания. Започнете да управлявате вашите документи оттук",
|
||||
"subtitle": "Добре дошли в Центъра за ресурси. Започнете да управлявате вашите документи и файлове оттук.",
|
||||
"uploadEntries": {
|
||||
"files": {
|
||||
"title": "Качване на файлове"
|
||||
@@ -99,27 +118,27 @@
|
||||
"folder": {
|
||||
"title": "Качване на папка"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Създай база знания"
|
||||
"library": {
|
||||
"title": "Създаване на нова библиотека"
|
||||
},
|
||||
"newPage": {
|
||||
"title": "Създаване на нов документ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"library": {
|
||||
"list": {
|
||||
"confirmRemoveKnowledgeBase": "Сигурни ли сте, че искате да изтриете тази база знания? Файловете в нея няма да бъдат изтрити, а ще бъдат преместени в общите файлове. След изтриването на базата знания, тя не може да бъде възстановена, моля, действайте внимателно.",
|
||||
"empty": "Кликнете <1>+</1>, за да започнете създаването на база знания"
|
||||
"confirmRemoveLibrary": "Ще бъде изтрита тази библиотека. Файловете в нея няма да бъдат изтрити, а ще бъдат преместени във 'Всички файлове'. След изтриване библиотеката не може да бъде възстановена. Моля, действайте внимателно.",
|
||||
"empty": "Кликнете <1>+</1>, за да създадете библиотека"
|
||||
},
|
||||
"new": "Нова база знания",
|
||||
"title": "База знания"
|
||||
"new": "Библиотека",
|
||||
"title": "Библиотеки"
|
||||
},
|
||||
"menu": {
|
||||
"allFiles": "Всички файлове",
|
||||
"allPages": "Всички документи"
|
||||
},
|
||||
"networkError": "Неуспешно получаване на базата от знания, моля, проверете интернет връзката и опитайте отново",
|
||||
"networkError": "Неуспешно зареждане на библиотеките. Моля, проверете интернет връзката и опитайте отново.",
|
||||
"notSupportGuide": {
|
||||
"desc": "Текущият инстанс е в режим на клиентска база данни и не поддържа функцията за управление на файлове. Моля, превключете на <1>режим на сървърна база данни</1> или използвайте директно <3>LobeChat Cloud</3>",
|
||||
"features": {
|
||||
@@ -131,9 +150,9 @@
|
||||
"desc": "Използва високопроизводителни векторни модели за векторизация на текстови части, позволявайки семантично търсене на съдържанието на файловете",
|
||||
"title": "Семантична векторизация"
|
||||
},
|
||||
"repos": {
|
||||
"desc": "Поддържа създаване на база знания и позволява добавяне на различни типове файлове, за да изградите собствена област на знание",
|
||||
"title": "База знания"
|
||||
"libraries": {
|
||||
"desc": "Позволява създаване на библиотеки и добавяне на различни типове файлове, за да изградите собствена база от знания.",
|
||||
"title": "Библиотеки"
|
||||
}
|
||||
},
|
||||
"title": "Текущият режим на инсталация не поддържа управление на файлове"
|
||||
@@ -155,7 +174,7 @@
|
||||
"videos": "Видеа",
|
||||
"websites": "Уебсайтове"
|
||||
},
|
||||
"title": "База знания",
|
||||
"title": "Ресурси",
|
||||
"toggleLeftPanel": "Показване/скриване на лявия панел",
|
||||
"uploadDock": {
|
||||
"body": {
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
"desc": "Прегенерирайте последното съобщение",
|
||||
"title": "Прегенериране на съобщение"
|
||||
},
|
||||
"saveDocument": {
|
||||
"desc": "Запазете незабавно всички промени в текущия документ",
|
||||
"title": "Запазване на документа"
|
||||
},
|
||||
"saveTopic": {
|
||||
"desc": "Запазете текущата тема и отворете нова",
|
||||
"title": "Създаване на нова тема"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"addToKnowledgeBase": {
|
||||
"addSuccess": "Файлът беше добавен успешно, <1>прегледайте веднага</1>",
|
||||
"confirm": "Добави",
|
||||
"error": "Неуспешно добавяне на файл към базата знания",
|
||||
"id": {
|
||||
"placeholder": "Моля, изберете знание база за добавяне",
|
||||
"required": "Моля, изберете знание база",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"authorize": {
|
||||
"cancel": "Отказ",
|
||||
"confirm": "Разреши използването",
|
||||
"description": {
|
||||
"and": "и",
|
||||
"prefix": "С натискане на „Разреши използването“ се съгласявате с",
|
||||
"privacy": "Политиката за поверителност",
|
||||
"terms": "Условията за ползване"
|
||||
"confirm": "Създай своя профил",
|
||||
"description": "Твоят общностен профил съществува независимо от потребителския акаунт в {{appName}}.",
|
||||
"footer": {
|
||||
"agreement": "Продължавайки, потвърждаваш, че си запознат и приемаш <terms>Условията за ползване</terms> и <privacy>Политиката за поверителност</privacy>",
|
||||
"privacy": "Политика за поверителност",
|
||||
"terms": "Условия за ползване"
|
||||
},
|
||||
"title": "Потвърждаване на оторизацията"
|
||||
"subtitle": "Създай общностен профил, за да подаваш и управляваш информация за публикуване в общността.",
|
||||
"title": "Създаване на общностен профил"
|
||||
},
|
||||
"callback": {
|
||||
"buttons": {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"identity": {
|
||||
"empty": "Няма запаметени самоличности",
|
||||
"filter": {
|
||||
"search": "Търсене на роля, връзка или описание...",
|
||||
"type": {
|
||||
"all": "Всички",
|
||||
"demographic": "Демографски",
|
||||
"personal": "Лични",
|
||||
"professional": "Професионални"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"confirmDelete": "Потвърдете изтриването",
|
||||
"deleteCancel": "Отказ",
|
||||
"deleteContent": "Сигурни ли сте, че искате да изтриете тази самоличност? Това действие не може да бъде отменено.",
|
||||
"deleteOk": "Изтрий",
|
||||
"noResults": "Няма намерени съвпадащи самоличности",
|
||||
"updated": "Актуализирано"
|
||||
},
|
||||
"roleCloud": {
|
||||
"collapse": "Скрий",
|
||||
"expand": "Покажи още"
|
||||
},
|
||||
"view": {
|
||||
"list": "Списък",
|
||||
"timeline": "Хронология"
|
||||
}
|
||||
},
|
||||
"loading": "Зареждане..."
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user