Compare commits

..

178 Commits

Author SHA1 Message Date
Innei e7f4bb66e3 refactor: update useMenu logic and enhance tests for profile inclusion
- Modified the `useMenu` hook to conditionally include the profile item based on authentication status.
- Updated tests in `useMenu.test.tsx` to verify the presence of the profile item in the main menu items.

Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 18:15:00 +08:00
Innei c379be4461 chore: add global type definitions and refactor import statements
- Introduced a new global type definition file to support Vite client imports.
- Refactored import statements in `App.ts` and `App.test.ts` to remove unnecessary type casting for `import.meta.glob`, improving code clarity.

Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 17:35:27 +08:00
Innei d8967c7e27 refactor: enhance IPC method registration for improved type safety
- Updated `registerMethod` in `IpcHandler` and `IpcService` to accept variable argument types, enhancing flexibility in method signatures.
- Simplified the `ExtractMethodSignature` type to support multiple arguments.

Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 16:19:14 +08:00
Innei dcb917fb23 feat: enhance type-safe IPC flow with context propagation and service registry
- Introduced `getIpcContext()` and `runWithIpcContext()` for improved context management in IPC handlers.
- Updated `BrowserWindowsCtr` methods to utilize the new context handling.
- Added `McpInstallCtr` to the IPC constructors registry.
- Enhanced README with details on the new type-safe IPC features.

Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 16:01:46 +08:00
Innei cea5e1417c refactor: unify IPC mocking across test files for consistency
Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 16:01:45 +08:00
Innei 37e32bcfce fix: export FileMetadata interface for improved accessibility
Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 16:01:45 +08:00
Innei 80eb4e3505 chore: add new workspace for desktop application in package.json
Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 16:01:45 +08:00
Innei 6463c4d12a fix: cast IPC return type to DesktopIpcServices for type safety
Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 16:01:45 +08:00
Innei e65ae0ec2d refactor: update IPC method names for consistency
Signed-off-by: Innei <tukon479@gmail.com>
2025-12-08 16:01:45 +08:00
Innei b7cd3fa010 refactor: server ipc 2025-12-08 16:01:44 +08:00
Innei c444dca7f2 refactor: client ipc 2025-12-08 16:01:44 +08:00
Rene Wang 1025478de2 feat: Allow DND in tree 2025-12-08 15:18:17 +08:00
arvinxx 10141be2e5 push group route 2025-12-08 14:57:05 +08:00
arvinxx fed079ad0d refactor agent group 2025-12-08 14:49:23 +08:00
arvinxx 0d10292989 refactor create group 2025-12-08 14:49:22 +08:00
arvinxx 9e19e439f7 refactor create group 2025-12-08 14:49:22 +08:00
Rene Wang 96484851c0 feat: Page copilot 2025-12-08 14:42:28 +08:00
Shinji-Li 2b72f28be3 feat: support approve install plugins in agent builder (#10662)
* feat: update agent builder tools call way

* feat: add install Plugins tools & add human approve intervation
2025-12-08 14:29:45 +08:00
arvinxx 29a08bb673 support delete memory 2025-12-08 10:00:22 +08:00
arvinxx 55c7c439f8 improve start input action 2025-12-08 00:27:13 +08:00
arvinxx 501f1a9938 fix delete agent and improve start input action 2025-12-08 00:12:41 +08:00
arvinxx df580d79f8 fix create agent flow 2025-12-07 23:34:29 +08:00
arvinxx f4ea92399f refactor sessionStore to agentStore 2025-12-07 23:08:01 +08:00
Rene Wang 649640a538 lint: Clean up coed 2025-12-07 19:54:11 +08:00
arvinxx a1a7673d79 refactor Conversation with agentStore 2025-12-07 19:38:58 +08:00
arvinxx eefb14072e refactor home page using homeStore and agentStore 2025-12-07 19:07:54 +08:00
arvinxx b59577c2d2 refactor sidebar using homeStore and agentStore 2025-12-07 18:43:26 +08:00
arvinxx a89581216f refactor agent using homeStore and agentStore 2025-12-07 18:43:26 +08:00
arvinxx 6b7744e6a6 home store 2025-12-07 18:43:26 +08:00
canisminor1990 acd0909cb2 style: fix auth card 2025-12-07 18:24:51 +08:00
canisminor1990 df71b12f1a style: update memory card 2025-12-07 18:17:39 +08:00
Neko 5666e474dd test(database): document model delete all (#10644) 2025-12-07 17:32:45 +08:00
canisminor1990 20c5a8f80c style: update memory card 2025-12-07 17:16:32 +08:00
arvinxx 9c094bc56c Home Repo 2025-12-07 16:29:24 +08:00
canisminor1990 c80e120328 style: update memory GroupedVirtuoso 2025-12-07 15:34:51 +08:00
Rene Wang 1b0d02809c feat: Optimize hook 2025-12-07 15:02:17 +08:00
canisminor1990 1a2055c0e6 style: update memory 2025-12-07 14:49:07 +08:00
Rene Wang f15c3cb939 lint: Clean up codes 2025-12-07 12:59:58 +08:00
canisminor1990 0078695da6 style: update memory 2025-12-07 12:12:47 +08:00
arvinxx 8b6b1eb995 refactor send 2025-12-07 12:06:36 +08:00
arvinxx 2abccbb47a support switch input mode 2025-12-07 12:04:46 +08:00
Rene Wang 3b1b497d3e refac: Reorgnize folders 2025-12-07 11:58:46 +08:00
Rene Wang 1267ed9f38 lint: Remove unused files 2025-12-07 11:49:59 +08:00
Rene Wang bfbc80b7f7 refac: Clean up code 2025-12-07 11:34:52 +08:00
Rene Wang 4fe52294c0 opti: Better dnd performance 2025-12-07 11:25:38 +08:00
Rene Wang 5dedb3eda0 opti: Better D & D performance 2025-12-07 11:11:39 +08:00
Rene Wang 9eecc9becc refac: Clean up code 2025-12-07 11:01:46 +08:00
arvinxx 28e1c5f091 fix build 2025-12-07 10:41:26 +08:00
arvinxx 9904fc41cc try to fix build 2025-12-07 10:25:26 +08:00
Neko ee86fd6058 ♻️ refactor(memory-user-memory): better structure, added tests, simplified executor (#10641) 2025-12-07 09:33:18 +08:00
Neko 2f0454719b 🔨 chore(userMemories): improved the results of memory extractor (#10636) 2025-12-07 02:04:24 +08:00
canisminor1990 fa7ccaa353 chore: rm unused loading 2025-12-07 00:45:49 +08:00
canisminor1990 90b1d19c77 style: update market-auth-callback 2025-12-07 00:39:24 +08:00
arvinxx fcbdfffa6d Context 情景记忆 2025-12-07 00:37:04 +08:00
canisminor1990 3c68226bce style: update oidc style 2025-12-07 00:31:36 +08:00
arvinxx 7ed30fc881 Preference 偏好记忆 2025-12-07 00:22:37 +08:00
arvinxx 80b230672b Experience 经验记忆 2025-12-06 23:59:13 +08:00
arvinxx 3508deff3e Experience 经验记忆 2025-12-06 23:50:29 +08:00
canisminor1990 19b09e069f style: update market-auth-callback 2025-12-06 23:39:45 +08:00
Rene Wang 53b4b91af6 refac: Clean up code 2025-12-06 23:20:07 +08:00
Rene Wang 8719744225 fix: back button 2025-12-06 23:05:59 +08:00
Rene Wang 120e01d8e7 fix: View mode 2025-12-06 23:03:27 +08:00
Rene Wang 443dd88446 fix: Update library id based on URL 2025-12-06 23:03:27 +08:00
canisminor1990 65bae726c0 fix: fix home market avatar z-index 2025-12-06 22:58:53 +08:00
Rene Wang 89b96b5e8e refac: State & UI 2025-12-06 22:55:31 +08:00
canisminor1990 301908f377 fix: roll back file pagesize 2025-12-06 21:30:37 +08:00
Rene Wang 19a9e88ffc style: Update header 2025-12-06 20:44:41 +08:00
Rene Wang 5237e045ea fix: Root folder 2025-12-06 20:42:18 +08:00
canisminor1990 35f19d9b31 fix: fix mobile 2025-12-06 20:30:40 +08:00
canisminor1990 7352b8a16b fix: fix mobile 2025-12-06 20:24:21 +08:00
canisminor1990 50e386b43f fix: fix mobile 2025-12-06 20:18:46 +08:00
Shinji-Li 1bfe4579bb feat: add more official tools into agentbuilder (#10638)
*  feat: add klavis tools into agent builder

- Add searchOfficialTools API for searching builtin and Klavis integrations
- Add searchMarketTools API for searching marketplace plugins
- Update AgentTool.tsx with dual-column Segmented tabs layout (All/Installed)
- Update PluginTag.tsx to support Klavis tools display with proper icons
- Fix pre-existing type errors in Header.tsx and pluginTypes.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* 🐛 fix: resolve circular dependencies in klavisStore imports

- Import KlavisServerStatus from types.ts instead of index.ts
- Import selectors from @/store/tool/selectors instead of slice paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-06 19:37:24 +08:00
canisminor1990 60ca998aac style: update res pagesize 2025-12-06 18:53:25 +08:00
canisminor1990 38dc1a69a4 style: update agent setting 2025-12-06 18:09:16 +08:00
Neko 3551ab8f64 fix(userMemories): Upstash Workflows will serialize into JSON, causes the Date being incorrectly used (#10635) 2025-12-06 15:24:56 +08:00
canisminor1990 e82cb62109 style: update agent publish 2025-12-06 15:00:55 +08:00
Shinji-Li 5760f550ed feat: add modify system prompt tools in agentbuilder (#10634)
* feat: support modify system role & use stream output

* fix: slove metion dropdown position
2025-12-06 15:00:01 +08:00
canisminor1990 4222768078 style: update page sidebar 2025-12-06 15:00:01 +08:00
canisminor1990 e9bd57eeb3 feat: add Suspense debug 2025-12-06 15:00:01 +08:00
canisminor1990 7a003b0d37 feat: add Suspense debug 2025-12-06 15:00:01 +08:00
Neko 26cf7d7308 🐛 fix: expose /api/workflows endpoints (#10632) 2025-12-06 15:00:01 +08:00
canisminor1990 f9bb091f3a fix: fix DraggablePanel 2025-12-06 15:00:01 +08:00
canisminor1990 aab42b4087 fix: fix cmd k register 2025-12-06 15:00:01 +08:00
canisminor1990 f253881fc0 fix: fix auto save 2025-12-06 14:59:59 +08:00
canisminor1990 2b0e4aed44 style: update editor 2025-12-06 14:59:52 +08:00
Neko 23fc9c50d3 feat(userMemories): unify layer names, add webhook verify headers (#10629) 2025-12-06 14:59:52 +08:00
canisminor1990 9bd27ea414 style: update editor 2025-12-06 14:59:52 +08:00
canisminor1990 6f1cfa9480 style: update editor 2025-12-06 14:59:51 +08:00
canisminor1990 55194e7986 style: update agent builder 2025-12-06 14:59:51 +08:00
canisminor1990 2aa28fe8bc fix: DraggablePanel 2025-12-06 14:59:51 +08:00
canisminor1990 7001a2ea03 fix: DraggablePanel 2025-12-06 14:59:51 +08:00
arvinxx b29581d69d refactor the switch branch 2025-12-06 14:59:51 +08:00
arvinxx e2e70f1121 fix topic messages issues 2025-12-06 14:59:51 +08:00
canisminor1990 cbd1d0f584 style: update auth 2025-12-06 14:59:51 +08:00
Neko f2745306af feat(userMemories): whitelist to extract users (#10626)
feat(userMemories): whitelist to extract users for
2025-12-06 14:59:51 +08:00
Shinji-Li d6e989d692 feat: add more acions & can modify provider and model (#10625)
* feat: add topic selectror

* feat: add model change & history change way

* feat: add more tools
2025-12-06 14:59:51 +08:00
Shinji-Li de2f7f3a20 🔨 chore: change the settings sub router to / path (#10617)
feat: change the settings sub router to / path
2025-12-06 14:59:51 +08:00
Rene Wang 58adbdd983 feat: Support blocks 2025-12-06 14:59:51 +08:00
Rene Wang a762049b62 fix: Update filter 2025-12-06 14:59:51 +08:00
arvinxx 762127860d fix topic models update issues 2025-12-06 14:59:51 +08:00
Neko 43c834d687 feat(userMemory): with Upstash Workflows for memory extractor (#10623)
feat(userMemory): with Upstash Workflows for memory extractor
2025-12-06 14:59:50 +08:00
arvinxx 0a7ba6bf0b support send message 2025-12-06 14:59:50 +08:00
Neko dec483dccb ♻️ refactor(server/modules/s3): improved constructor to accept options without taking env directly (#10624)
refactor(server/modules/s3): improved constructor to accept options without taking env directly
2025-12-06 14:59:50 +08:00
Rene Wang 0a9a8ab817 fix: Exclude mapped document 2025-12-06 14:59:50 +08:00
Rene Wang d59694bb08 feat: Header 2025-12-06 14:59:50 +08:00
Rene Wang 6088095119 feat: Renaming KB 2025-12-06 14:59:50 +08:00
Rene Wang 75d591e779 fix: Changelog dialog crashing 2025-12-06 14:59:50 +08:00
arvinxx e1ddb27ef9 refactor model list 2025-12-06 14:59:50 +08:00
arvinxx b5a30c8359 update tests 2025-12-06 14:59:50 +08:00
Rene Wang 20791df887 fix: Change log modal 2025-12-06 14:59:50 +08:00
Rene Wang e4bfeb00d0 fix: Update translation 2025-12-06 14:59:50 +08:00
arvinxx 2d3bd01b02 refactor chat input issue 2025-12-06 14:59:48 +08:00
Rene Wang 9f1f23125b style: Changelog modal 2025-12-06 14:59:09 +08:00
Rene Wang d6a69f9b5b feat: Download document 2025-12-06 14:59:09 +08:00
Rene Wang dd503ff418 fix: Remove unncessary tRPC calling 2025-12-06 14:59:09 +08:00
arvinxx 882850f56b refactor topic issues 2025-12-06 14:59:09 +08:00
canisminor1990 28a7796cec style: fix cursor 2025-12-06 14:59:08 +08:00
Shinji-Li f96e3bda5d 🔨 chore: delete the url hydration & romove the pin agent way (#10616)
fix: delete the url Hydration & delete pinagent way
2025-12-06 14:59:08 +08:00
Rene Wang 9a241af65d fix: Deduplication 2025-12-06 14:59:08 +08:00
canisminor1990 2942f2244f style: fix cursor 2025-12-06 14:59:08 +08:00
canisminor1990 bc0a7a14d8 style: update editor style 2025-12-06 14:59:08 +08:00
canisminor1990 e8314145e0 style: update style 2025-12-06 14:59:08 +08:00
arvinxx dee6e5f97a fix 2025-12-06 14:59:08 +08:00
Rene Wang 031f8d143b feat: Discard the page editor modal 2025-12-06 14:59:07 +08:00
Rene Wang dc31d02bcd refac: Renaming files 2025-12-06 14:59:07 +08:00
Rene Wang 68394609b7 fix: Page explore 2025-12-06 14:59:07 +08:00
Rene Wang a6263a45d2 opti: Better file loading 2025-12-06 14:59:07 +08:00
canisminor1990 62c6d12192 chore: add knip cli 2025-12-06 14:59:07 +08:00
arvinxx 1c9c229e2a ♻️ refactor(agent): add getAgentConfigById to AgentService with default config merging
- Add getAgentConfigById method to AgentService that merges default configs
- Extract mergeDefaultConfig as private helper method for code reuse
- Update router to use agentService.getAgentConfigById instead of agentModel
- Update updateAgentConfig to return merged config via getAgentConfigById
- Add tests for getAgentConfigById merging behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 14:59:07 +08:00
arvinxx 13fedf67f6 fix 2025-12-06 14:59:07 +08:00
arvinxx 976d63b099 🐛 fix(agent): merge default configs for builtin agents on server side
- Add DEFAULT_AGENT_CONFIG and serverDefaultAgentConfig merging in AgentService.getBuiltinAgent
- Ensures inbox agent always has complete config with model/provider
- Update selector comment to document server-side merging behavior
- Add unit tests for config merging behavior

Closes LOBE-1447

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 14:59:07 +08:00
canisminor1990 69966cb235 style: update chat item style 2025-12-06 14:59:07 +08:00
canisminor1990 d10ade40f9 style: update chat item style 2025-12-06 14:59:07 +08:00
canisminor1990 7cbade75bd style: update new topic button 2025-12-06 14:59:07 +08:00
canisminor1990 4c894a3b98 style: update agent welcome and chatinput 2025-12-06 14:59:07 +08:00
canisminor1990 d286c29745 style: update agent welcome and chatinput 2025-12-06 14:59:07 +08:00
Rene Wang 561e80050f opti: Save on blur 2025-12-06 14:59:07 +08:00
canisminor1990 6c9b46e8df fix: systemrole editor init 2025-12-06 14:59:07 +08:00
Shinji-Li 6a955cc3ea 🐛 fix: slove the command k jump link error (#10614)
fix: fixed command k router error
2025-12-06 14:59:07 +08:00
Rene Wang 7d2ea6b243 fix: Jump link 2025-12-06 14:59:07 +08:00
Rene Wang ee36f3210b fix: Copilot size 2025-12-06 14:59:06 +08:00
arvinxx 4ab7013310 update for test 2025-12-06 14:59:06 +08:00
arvinxx 2243f82ba7 update for test 2025-12-06 14:59:06 +08:00
arvinxx 3656e162fe fix topic with inbox agent 2025-12-06 14:59:06 +08:00
arvinxx 05eb57ec3c refactor agent 2025-12-06 14:59:06 +08:00
arvinxx d97138fab3 refactor agent 2025-12-06 14:59:06 +08:00
canisminor1990 af14b2fb04 style: update icon 2025-12-06 14:59:06 +08:00
canisminor1990 3ecb3c4fe0 style: update create icon 2025-12-06 14:59:06 +08:00
Rene Wang e563be6a8c style: Search in CMDK 2025-12-06 14:59:06 +08:00
Rene Wang 7bb6061a03 refac: Lint code style 2025-12-06 14:59:06 +08:00
Rene Wang fff64ea919 style: Adjust padding 2025-12-06 14:59:06 +08:00
Neko 38da250e5d feat(userMemories): extract from user memory, add new memory-user-memory package (#10514)
* feat(userMemories): extract from user memory, add new memory-user-memory package

* chore: missing deps

* chore: missing test
2025-12-06 14:59:06 +08:00
arvinxx 0b99552ada inbox 2025-12-06 14:59:06 +08:00
arvinxx e805e8cb96 refactor inbox agent store 2025-12-06 14:59:05 +08:00
arvinxx 37aabb7bd5 support session get inbox 2025-12-06 14:59:05 +08:00
Rene Wang 122cd6294c feat: Support JS rendering 2025-12-06 14:59:05 +08:00
canisminor1990 3a4d9ce33e pref: update topic count 2025-12-06 14:59:05 +08:00
Rene Wang f291505215 feat: Handle gitignore 2025-12-06 14:59:05 +08:00
arvinxx 79aab68167 add welcome for agent 2025-12-06 14:59:05 +08:00
Rene Wang 356efab490 feat: Upload folder 2025-12-06 14:59:05 +08:00
arvinxx 8079032563 improve conversation width 2025-12-06 14:59:05 +08:00
arvinxx efc8d83221 support config plugins 2025-12-06 14:59:05 +08:00
Rene Wang ff06a3c602 feat: Unfiied search 2025-12-06 14:59:05 +08:00
canisminor1990 2ed15c50af style: update style 2025-12-06 14:59:05 +08:00
canisminor1990 1fe0b2e9f3 style: clean add button 2025-12-06 14:59:05 +08:00
canisminor1990 81400f3bdb feat: add agent more 2025-12-06 14:59:05 +08:00
canisminor1990 c14d2781dd feat: add agent more 2025-12-06 14:59:04 +08:00
Rene Wang 46c8106185 style: Turn changelog to a modal 2025-12-06 14:59:04 +08:00
canisminor1990 26355231ec feat: add agent more 2025-12-06 14:59:04 +08:00
canisminor1990 92a8fc1d38 feat: add topic more 2025-12-06 14:59:04 +08:00
Arvin Xu 425b773769 memory-panel (#10598)
* 🐛 fix: missing init user after user creation (#10587)

* 🌐 chore: translate non-English comments to English in python-interpreter (#10568)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* 基本完成简单的 memory 提取

finish extract

identity 提取

refactor prompts and implements

add memory layers

refactor GateKeeper generate

improve GateKeeper generate

init packages

add messages prompts

 feat(memory): add filter, search and i18n for identity panel

- Add demographic identity type support
- Implement complete i18n with memory namespace
- Add type filter (personal/professional/demographic) and search functionality
- Remove Preference and Experience features (keep only Identity and Context)
- Optimize UI layout: left-right filter/view mode layout
- Add collapsible role tag cloud
- Refactor types to use strict IdentityType enum
- Fix type definitions and imports to use @lobechat/types consistently

finish Identity ui

update

wip for memory panel

* update

* rename

* refactor memory

* update
2025-12-06 14:59:04 +08:00
canisminor1990 721b8c980d style: update welcome speed 2025-12-06 14:59:04 +08:00
Rene Wang aaf264ca0f feat: Move help center to the footer 2025-12-06 14:59:04 +08:00
Rene Wang a807d658f3 style: Add tooltip 2025-12-06 14:59:04 +08:00
Rene Wang fdd63c25c7 feat: New AddButton 2025-12-06 14:59:04 +08:00
canisminor1990 4f88b498f7 style: update i18n 2025-12-06 14:59:04 +08:00
arvinxx 2967f36805 fix types 2025-12-06 14:59:03 +08:00
Shinji-Li cfb2ced431 style: update layout
 test: update tests (#10510)

♻️ refactor: refactor update agent config implement (#10507)

♻️ refactor: refactor session store to agent store (#10485)

♻️ refactor: refactor with new conversation store (#10483)

 feat: add editor data into market agent (#10451)

feat: Create Folder in Repo (#10352)

🔨 chore: delete editor content sql migration (#10449)

💄 style(wip): LobeHub Next UI Refactor (#10388)

 feat: change agent settings drawer to editor mode (#10392)

* 🐛 fix: Showing compatibility with both new and old versions of Plugins (#10418)

---------

Co-authored-by: Arvin Xu <arvinx@foxmail.com>
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-12-06 14:58:59 +08:00
1246 changed files with 28263 additions and 50357 deletions
@@ -1,275 +0,0 @@
# Agent Runtime E2E 测试指南
本文档描述 Agent Runtime 端到端测试的核心原则和实施方法。
## 核心原则
### 1. 最小化 Mock 原则
E2E 测试的目标是尽可能接近真实运行环境。因此,我们只 Mock **三个外部依赖**:
| 依赖 | Mock 方式 | 说明 |
|------|----------|------|
| **Database** | PGLite | 使用 `@lobechat/database/test-utils` 提供的内存数据库 |
| **Redis** | InMemoryAgentStateManager | Mock `AgentStateManager` 使用内存实现 |
| **Redis** | InMemoryStreamEventManager | Mock `StreamEventManager` 使用内存实现 |
**不 Mock 的部分:**
- `model-bank` - 使用真实的模型配置数据
- `Mecha` (AgentToolsEngine, ContextEngineering) - 使用真实逻辑
- `AgentRuntimeService` - 使用真实逻辑
- `AgentRuntimeCoordinator` - 使用真实逻辑
### 2. 使用 vi.spyOn 而非 vi.mock
不同测试场景需要不同的 LLM 响应。使用 `vi.spyOn` 可以:
- 在每个测试中灵活控制返回值
- 便于测试不同场景(纯文本、tool calls、错误等)
- 避免全局 mock 导致的测试隔离问题
### 3. 默认模型使用 gpt-5
- `model-bank` 中肯定有该模型的数据
- 避免短期内因模型更新需要修改测试
## 技术实现
### 数据库设置
```typescript
import { LobeChatDatabase } from '@lobechat/database';
import { getTestDB } from '@lobechat/database/test-utils';
let testDB: LobeChatDatabase;
beforeEach(async () => {
testDB = await getTestDB();
});
```
### OpenAI Response Mock Helper
创建一个 helper 函数来生成 OpenAI 格式的流式响应:
```typescript
/**
* 创建 OpenAI 格式的流式响应
*/
export const createOpenAIStreamResponse = (options: {
content?: string;
toolCalls?: Array<{
id: string;
name: string;
arguments: string;
}>;
finishReason?: 'stop' | 'tool_calls';
}) => {
const { content, toolCalls, finishReason = 'stop' } = options;
return new Response(
new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
// 发送内容 chunk
if (content) {
const chunk = {
id: 'chatcmpl-mock',
object: 'chat.completion.chunk',
model: 'gpt-5',
choices: [{
index: 0,
delta: { content },
finish_reason: null,
}],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
// 发送 tool_calls chunk
if (toolCalls) {
for (const tool of toolCalls) {
const chunk = {
id: 'chatcmpl-mock',
object: 'chat.completion.chunk',
model: 'gpt-5',
choices: [{
index: 0,
delta: {
tool_calls: [{
index: 0,
id: tool.id,
type: 'function',
function: {
name: tool.name,
arguments: tool.arguments,
},
}],
},
finish_reason: null,
}],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
}
// 发送完成 chunk
const finishChunk = {
id: 'chatcmpl-mock',
object: 'chat.completion.chunk',
model: 'gpt-5',
choices: [{
index: 0,
delta: {},
finish_reason: finishReason,
}],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
}),
{ headers: { 'content-type': 'text/event-stream' } },
);
};
```
### 内存状态管理
使用依赖注入替代 Redis
```typescript
import {
InMemoryAgentStateManager,
InMemoryStreamEventManager,
} from '@/server/modules/AgentRuntime';
import { AgentRuntimeService } from '@/server/services/agentRuntime';
const stateManager = new InMemoryAgentStateManager();
const streamEventManager = new InMemoryStreamEventManager();
const service = new AgentRuntimeService(serverDB, userId, {
coordinatorOptions: {
stateManager,
streamEventManager,
},
queueService: null, // 禁用 QStash 队列,使用 executeSync
streamEventManager,
});
```
### Mock OpenAI API
在测试中使用 `vi.spyOn` mock fetch
```typescript
import { vi } from 'vitest';
// 在测试文件顶部或 beforeEach 中
const fetchSpy = vi.spyOn(globalThis, 'fetch');
// 在具体测试中设置返回值
it('should handle text response', async () => {
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({ content: '杭州今天天气晴朗' })
);
// ... 执行测试
});
it('should handle tool calls', async () => {
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({
toolCalls: [{
id: 'call_123',
name: 'lobe-web-browsing____search____builtin',
arguments: JSON.stringify({ query: '杭州天气' }),
}],
finishReason: 'tool_calls',
})
);
// ... 执行测试
});
```
## 测试场景
### 1. 基本对话测试
```typescript
describe('Basic Chat', () => {
it('should complete a simple conversation', async () => {
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({ content: 'Hello! How can I help you?' })
);
const result = await service.createOperation({
agentConfig: { model: 'gpt-5', provider: 'openai' },
initialMessages: [{ role: 'user', content: 'Hi' }],
// ...
});
const finalState = await service.executeSync(result.operationId);
expect(finalState.status).toBe('done');
});
});
```
### 2. Tool 调用测试
```typescript
describe('Tool Calls', () => {
it('should execute web-browsing tool', async () => {
// 第一次调用:LLM 返回 tool_calls
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({
toolCalls: [{
id: 'call_123',
name: 'lobe-web-browsing____search____builtin',
arguments: JSON.stringify({ query: '杭州天气' }),
}],
finishReason: 'tool_calls',
})
);
// 第二次调用:处理 tool 结果后的响应
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({ content: '根据搜索结果,杭州今天...' })
);
// ... 执行测试
});
});
```
### 3. 错误处理测试
```typescript
describe('Error Handling', () => {
it('should handle API errors gracefully', async () => {
fetchSpy.mockRejectedValueOnce(new Error('API rate limit exceeded'));
// ... 执行测试并验证错误处理
});
});
```
## 文件组织
```
src/server/routers/lambda/__tests__/integration/
├── setup.ts # 测试设置工具
├── aiAgent.integration.test.ts # 现有集成测试
├── aiAgent.e2e.test.ts # E2E 测试
└── helpers/
└── openaiMock.ts # OpenAI mock helper
```
## 注意事项
1. **测试隔离**:每个测试后清理 `InMemoryAgentStateManager` 和 `InMemoryStreamEventManager`
2. **超时设置**:E2E 测试可能需要更长的超时时间
3. **调试**:使用 `DEBUG=lobe-server:*` 环境变量查看详细日志
-52
View File
@@ -2,58 +2,6 @@
# Changelog
## [Version 2.0.0-next.164](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.163...v2.0.0-next.164)
<sup>Released on **2025-12-08**</sup>
#### 💄 Styles
- **profile**: Add mobile responsive layout and signup improvements.
- **misc**: Update link handling in PlanTag component to use react-router-dom.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **profile**: Add mobile responsive layout and signup improvements, closes [#10669](https://github.com/lobehub/lobe-chat/issues/10669) ([1afd471](https://github.com/lobehub/lobe-chat/commit/1afd471))
- **misc**: Update link handling in PlanTag component to use react-router-dom, closes [#10673](https://github.com/lobehub/lobe-chat/issues/10673) ([3aceeb6](https://github.com/lobehub/lobe-chat/commit/3aceeb6))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.0.0-next.163](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.162...v2.0.0-next.163)
<sup>Released on **2025-12-06**</sup>
#### 🐛 Bug Fixes
- **misc**: Add smooth scroll to top on 'More' button click in Title component.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Add smooth scroll to top on 'More' button click in Title component, closes [#10178](https://github.com/lobehub/lobe-chat/issues/10178) ([5ad4f0c](https://github.com/lobehub/lobe-chat/commit/5ad4f0c))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.0.0-next.162](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.161...v2.0.0-next.162)
<sup>Released on **2025-12-05**</sup>
-2
View File
@@ -44,8 +44,6 @@ see @.cursor/rules/typescript.mdc
- wrap the file path in single quotes to avoid shell expansion
- Never run `bun run test` etc to run tests, this will run all tests and cost about 10mins
- If trying to fix the same test twice, but still failed, stop and ask for help.
- **Prefer `vi.spyOn` over `vi.mock`**: When mocking modules or functions, prefer using `vi.spyOn` to mock specific functions rather than `vi.mock` to mock entire modules. This approach is more targeted, easier to maintain, and allows for better control over mock behavior in individual tests.
- **Tests must pass type check**: After writing or modifying tests, run `bun run type-check` to ensure there are no type errors. Tests should pass both runtime execution and TypeScript type checking.
### Typecheck
+6 -6
View File
@@ -345,12 +345,12 @@ In addition, these plugins are not limited to news aggregation, but can also ext
<!-- PLUGIN LIST -->
| Recent Submits | Description |
| --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [AladinBooks](https://lobechat.com/discover/plugin/AladinSearchBooks)<br/><sup>By **azurewebsites** on **2025-12-08**</sup> | Search for books on Aladin.<br/>`book` `search` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | Enter any URL and keyword and get an On-Page SEO analysis & insights!<br/>`seo` |
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
| Recent Submits | Description |
| -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | Enter any URL and keyword and get an On-Page SEO analysis & insights!<br/>`seo` |
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
> 📊 Total plugins: [<kbd>**41**</kbd>](https://lobechat.com/discover/plugins)
+6 -6
View File
@@ -338,12 +338,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
<!-- PLUGIN LIST -->
| 最近新增 | 描述 |
| --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [AladinBooks](https://lobechat.com/discover/plugin/AladinSearchBooks)<br/><sup>By **azurewebsites** on **2025-12-08**</sup> | 在阿拉丁上搜索书籍。<br/>`书籍` `搜索` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | 输入任何 URL 和关键词,获取页面 SEO 分析和见解!<br/>`seo` |
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
| 最近新增 | 描述 |
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | 输入任何 URL 和关键词,获取页面 SEO 分析和见解!<br/>`seo` |
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
> 📊 Total plugins: [<kbd>**41**</kbd>](https://lobechat.com/discover/plugins)
+38 -42
View File
@@ -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
View File
@@ -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
+26 -1
View File
@@ -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** - 具有状态参数验证的安全认证
+1
View File
@@ -39,6 +39,7 @@ export default defineConfig({
resolve: {
alias: {
'~common': resolve(__dirname, 'src/common'),
'@': resolve(__dirname, 'src/main'),
},
},
},
+4 -3
View File
@@ -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,
+5 -4
View File
@@ -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,
+7 -37
View File
@@ -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();
+5 -29
View File
@@ -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>;
+15 -47
View File
@@ -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);
});
+7
View File
@@ -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 };
+2
View File
@@ -0,0 +1,2 @@
// Export types for renderer/server to use
export type { DesktopIpcServices, DesktopServerIpcServices } from './controllers/registry';
+3
View File
@@ -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;
});
});
+10
View File
@@ -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"
}
}
+1 -1
View File
@@ -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');
});
});
+170
View File
@@ -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);
}
+11
View File
@@ -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;
};
+4 -1
View File
@@ -15,5 +15,8 @@ export const setupElectronApi = () => {
console.error(error);
}
contextBridge.exposeInMainWorld('electronAPI', { invoke, onStreamInvoke });
contextBridge.exposeInMainWorld('electronAPI', {
invoke,
onStreamInvoke,
});
};
+13 -16
View File
@@ -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: '' });
+2 -5
View File
@@ -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',
+4 -4
View File
@@ -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
}
+15 -5
View File
@@ -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"
]
}
-14
View File
@@ -1,18 +1,4 @@
[
{
"children": {
"improvements": ["Update link handling in PlanTag component to use react-router-dom."]
},
"date": "2025-12-08",
"version": "2.0.0-next.164"
},
{
"children": {
"fixes": ["Add smooth scroll to top on 'More' button click in Title component."]
},
"date": "2025-12-06",
"version": "2.0.0-next.163"
},
{
"children": {},
"date": "2025-12-05",
-3
View File
@@ -54,7 +54,6 @@
},
"betterAuth": {
"errors": {
"confirmPasswordRequired": "يرجى تأكيد كلمة المرور",
"emailExists": "هذا البريد الإلكتروني مسجّل بالفعل، يرجى تسجيل الدخول مباشرة",
"emailInvalid": "يرجى إدخال عنوان بريد إلكتروني صالح",
"emailNotRegistered": "هذا البريد الإلكتروني غير مسجل",
@@ -66,7 +65,6 @@
"passwordFormat": "يجب أن تحتوي كلمة المرور على أحرف وأرقام",
"passwordMaxLength": "يجب ألا تتجاوز كلمة المرور 64 حرفًا",
"passwordMinLength": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل",
"passwordMismatch": "كلمتا المرور غير متطابقتين",
"passwordRequired": "يرجى إدخال كلمة المرور",
"usernameNotRegistered": "اسم المستخدم هذا غير مسجل",
"usernameRequired": "يرجى إدخال اسم المستخدم"
@@ -127,7 +125,6 @@
"submit": "تسجيل الدخول"
},
"signup": {
"confirmPasswordPlaceholder": "يرجى تأكيد كلمة المرور",
"emailPlaceholder": "يرجى إدخال عنوان البريد الإلكتروني",
"error": "فشل التسجيل، يرجى المحاولة مرة أخرى",
"firstNamePlaceholder": "الاسم الأول",
-16
View File
@@ -3,22 +3,6 @@
"title": "النموذج"
},
"active": "نشط",
"agentBuilder": {
"installPlugin": {
"authRequired": "يتطلب مكون MCP السحابي تسجيل الدخول والمصادقة",
"cancel": "إلغاء",
"clickApproveToConnect": "انقر على \"الموافقة\" للاتصال وتفويض هذا التكامل",
"connectedAndEnabled": "تم الاتصال والتفعيل",
"connectionFailed": "فشل الاتصال",
"installFailed": "فشل التثبيت",
"installPlugin": "تثبيت المكون الإضافي",
"installToEnable": "قم بتثبيت هذا المكون الإضافي لتمكين المساعد",
"installedAndEnabled": "تم التثبيت والتفعيل",
"requiresAuth": "يتطلب تفويضًا، انقر على \"الموافقة\" للاتصال",
"retry": "إعادة المحاولة"
},
"welcome": "مرحبًا، أنا **Lobe AI**، خبير إعداد مساعدك الشخصي. أخبرني بنوع المساعد الذي تريده، وسأقوم بإعداده لك."
},
"agentDefaultMessage": "مرحبًا، أنا **{{name}}**، يمكنك بدء المحادثة معي على الفور، أو يمكنك الذهاب إلى [إعدادات المساعد]({{url}}) لإكمال معلوماتي.",
"agentDefaultMessageWithSystemRole": "مرحبًا، أنا **{{name}}**، كيف يمكنني مساعدتك؟",
"agentDefaultMessageWithoutEdit": "مرحبًا، أنا **{{name}}**، كيف يمكنني مساعدتك؟",
+1 -14
View File
@@ -58,7 +58,6 @@
"title": "سجل الإصدارات"
}
},
"downloads": "عدد التنزيلات",
"list": "قائمة المساعدين",
"marketSource": {
"label": "تبديل مصدر المجتمع",
@@ -693,18 +692,6 @@
"home": "الصفحة الرئيسية",
"model": "النموذج",
"plugin": "الإضافة",
"provider": "مزود النموذج",
"user": "المستخدم"
},
"user": {
"agents": "المساعدون",
"downloads": "التنزيلات",
"editProfile": "تعديل الملف الشخصي",
"login": "تسجيل الدخول",
"logout": "تسجيل الخروج",
"myProfile": "صفحتي الشخصية",
"noAgents": "لم يقم هذا المستخدم بنشر أي مساعدين بعد",
"publishedAgents": "المساعدون المنشورون",
"website": "الموقع الشخصي"
"provider": "مزود النموذج"
}
}
-14
View File
@@ -86,20 +86,6 @@
},
"newFolder": "إنشاء مجلد جديد",
"newPage": "مستند جديد",
"notion": {
"error": "فشل في استيراد ملف Notion",
"foundFiles": "تم العثور على {{count}} ملف",
"importing": "جارٍ استيراد ملفات Notion...",
"noMarkdownFiles": "لم يتم العثور على ملفات Markdown في ملف ZIP",
"partial": "تم استيراد {{success}} ملفًا بنجاح، وفشل {{failed}} ملفًا",
"success": "تم استيراد {{count}} ملفًا بنجاح"
},
"notionGuide": {
"cancel": "إلغاء الاستيراد الآن",
"desc": "يرجى أولاً تصدير ملفات Markdown (بصيغة ZIP) من Notion، ثم النقر على متابعة لاختيار ملف الضغط واستيراد جميع الصفحات.",
"ok": "اختر ملف ZIP من Notion",
"title": "استيراد محتوى Notion"
},
"uploadFile": "رفع ملف",
"uploadFolder": "رفع مجلد"
},
-9
View File
@@ -1,9 +0,0 @@
{
"starter": {
"createAgent": "إنشاء مساعد",
"deepResearch": "بحث معمق",
"developing": "قيد التطوير",
"image": "رسم",
"write": "كتابة"
}
}
+4 -4
View File
@@ -70,12 +70,12 @@
"title": "تبديل المساعد بسرعة"
},
"toggleLeftPanel": {
"desc": "إظهار أو إخفاء اللوحة الجانبية اليسرى",
"title": "إظهار/إخفاء اللوحة الجانبية اليسرى"
"desc": "عرض أو إخفاء لوحة المساعد على اليسار",
"title": "عرض/إخفاء لوحة المساعد"
},
"toggleRightPanel": {
"desc": "إظهار أو إخفاء اللوحة الجانبية اليمنى",
"title": "إظهار/إخفاء اللوحة الجانبية اليمنى"
"desc": "عرض أو إخفاء لوحة المواضيع على اليمين",
"title": "عرض/إخفاء لوحة الموضوع"
},
"toggleZenMode": {
"desc": "في وضع التركيز، عرض المحادثة الحالية فقط، وإخفاء واجهة المستخدم الأخرى",
-37
View File
@@ -52,42 +52,5 @@
"submit": "تم التفويض بنجاح! يمكنك الآن نشر المساعد.",
"upload": "تم التفويض بنجاح! يمكنك الآن نشر إصدار جديد."
}
},
"profileSetup": {
"cancel": "إلغاء",
"descriptionEdit": "حدّث معلومات ملفك الشخصي في المجتمع.",
"descriptionFirstTime": "قم بإعداد ملفك الشخصي لإكمال إنشاء ملف المجتمع.",
"errors": {
"notAuthenticated": "يرجى تسجيل الدخول أولاً قبل المتابعة",
"updateFailed": "فشل في تحديث الملف الشخصي، يرجى المحاولة مرة أخرى",
"usernameTaken": "معرّف المستخدم هذا مستخدم بالفعل، يرجى اختيار معرّف آخر"
},
"fields": {
"description": {
"label": "نبذة شخصية",
"maxLength": "النبذة الشخصية يجب ألا تتجاوز 200 حرف",
"placeholder": "قدّم نفسك بإيجاز..."
},
"displayName": {
"label": "الاسم الظاهر",
"maxLength": "الاسم الظاهر يجب ألا يتجاوز 50 حرفًا",
"placeholder": "أدخل اسمك الظاهر",
"required": "يرجى إدخال الاسم الظاهر"
},
"userName": {
"label": "معرّف المستخدم",
"maxLength": "معرّف المستخدم يجب ألا يتجاوز 32 حرفًا",
"minLength": "معرّف المستخدم يجب أن لا يقل عن 3 أحرف",
"pattern": "يمكن أن يحتوي معرّف المستخدم على أحرف وأرقام وشرطات سفلية وشرطات فقط",
"placeholder": "أدخل معرّف المستخدم الخاص بك",
"required": "يرجى إدخال معرّف المستخدم",
"tooltip": "معرّف المستخدم هو معرفك الفريد وسيُستخدم في رابط صفحتك الشخصية"
}
},
"getStarted": "ابدأ الآن",
"save": "حفظ",
"success": "تم تحديث الملف الشخصي بنجاح",
"titleEdit": "تعديل الملف الشخصي",
"titleFirstTime": "أكمل ملفك الشخصي"
}
}
+1 -61
View File
@@ -1,38 +1,4 @@
{
"context": {
"actions": {
"delete": "حذف",
"edit": "تعديل"
},
"defaultType": "سياق",
"deleteConfirm": "هل أنت متأكد من أنك تريد حذف هذه الذاكرة السياقية؟ لا يمكن التراجع عن هذا الإجراء.",
"deleteTitle": "حذف الذاكرة السياقية",
"description": "الوصف",
"empty": "لا توجد ذكريات سياقية حالياً",
"impact": "درجة التأثير",
"source": "المصدر",
"urgency": "درجة الإلحاح"
},
"experience": {
"actions": {
"delete": "حذف",
"edit": "تعديل"
},
"confidence": "درجة الثقة",
"defaultType": "تجربة",
"deleteConfirm": "هل أنت متأكد من أنك تريد حذف هذه الذاكرة التجريبية؟ لا يمكن التراجع عن هذا الإجراء.",
"deleteTitle": "حذف الذاكرة التجريبية",
"empty": "لا توجد ذكريات تجريبية حالياً",
"keyLearning": "التعلم الرئيسي",
"situation": "السياق",
"source": "المصدر",
"steps": {
"action": "الإجراء المتخذ",
"outcome": "النتيجة المحتملة",
"reasoning": "عملية التفكير",
"situation": "الخلفية السياقية"
}
},
"identity": {
"empty": "لا توجد ذاكرة هوية حالياً",
"filter": {
@@ -61,31 +27,5 @@
"timeline": "الجدول الزمني"
}
},
"loading": "جارٍ التحميل...",
"preference": {
"actions": {
"delete": "حذف",
"edit": "تعديل"
},
"conclusionDirectives": "توجيهات الاستنتاج",
"defaultType": "تفضيل",
"deleteConfirm": "هل أنت متأكد من أنك تريد حذف هذه الذاكرة التفضيلية؟ لا يمكن التراجع عن هذا الإجراء.",
"deleteTitle": "حذف الذاكرة التفضيلية",
"empty": "لا توجد ذكريات تفضيلية حالياً",
"priority": "الأولوية",
"source": "المصدر",
"suggestions": "الاقتراحات"
},
"tab": {
"contexts": "السياقات",
"experiences": "التجارب",
"home": "الرئيسية",
"identities": "الهويات",
"preferences": "التفضيلات",
"search": "بحث"
},
"viewMode": {
"masonry": "عرض متدرج",
"timeline": "الجدول الزمني"
}
"loading": "جارٍ التحميل..."
}
-55
View File
@@ -1,25 +1,5 @@
{
"builtins": {
"lobe-agent-builder": {
"apiName": {
"getAvailableModels": "الحصول على النماذج المتاحة",
"getAvailableTools": "الحصول على الأدوات المتاحة",
"getConfig": "الحصول على الإعدادات",
"getMeta": "الحصول على البيانات الوصفية",
"getPrompt": "الحصول على التعليمات النظامية",
"searchMarketTools": "البحث في سوق الإضافات",
"searchOfficialTools": "البحث عن الأدوات الرسمية",
"setModel": "تعيين النموذج",
"setOpeningMessage": "تعيين رسالة البداية",
"setOpeningQuestions": "تعيين أسئلة البداية",
"togglePlugin": "تبديل الإضافة",
"updateChatConfig": "تحديث إعدادات المحادثة",
"updateConfig": "تحديث الإعدادات",
"updateMeta": "تحديث البيانات الوصفية",
"updatePrompt": "تحديث التعليمات النظامية"
},
"title": "منشئ الوكيل"
},
"lobe-knowledge-base": {
"apiName": {
"readKnowledge": "قراءة محتوى قاعدة المعرفة",
@@ -44,41 +24,6 @@
},
"title": "النظام المحلي"
},
"lobe-page-agent": {
"apiName": {
"batchUpdate": "تحديث الدُفعة للعُقد",
"compareSnapshots": "مقارنة اللقطات",
"convertToList": "تحويل إلى قائمة",
"createNode": "إنشاء عقدة",
"cropImage": "اقتصاص الصورة",
"deleteNode": "حذف العقدة",
"deleteSnapshot": "حذف اللقطة",
"deleteTableColumn": "حذف عمود الجدول",
"deleteTableRow": "حذف صف الجدول",
"duplicateNode": "نسخ العقدة",
"editTitle": "تحرير عنوان المستند",
"indentListItem": "زيادة المسافة البادئة لعنصر القائمة",
"initPage": "تهيئة المستند",
"insertTableColumn": "إدراج عمود في الجدول",
"insertTableRow": "إدراج صف في الجدول",
"listSnapshots": "عرض اللقطات",
"mergeNodes": "دمج العقد",
"moveNode": "نقل العقدة",
"outdentListItem": "تقليل المسافة البادئة لعنصر القائمة",
"replaceText": "استبدال النص",
"resizeImage": "تغيير حجم الصورة",
"restoreSnapshot": "استعادة اللقطة",
"rotateImage": "تدوير الصورة",
"saveSnapshot": "حفظ اللقطة",
"setImageAlt": "تعيين النص البديل للصورة",
"splitNode": "تقسيم العقدة",
"toggleListType": "تبديل نوع القائمة",
"unwrapNode": "إلغاء تغليف العقدة",
"updateNode": "تحديث العقدة",
"wrapNodes": "تغليف العقد"
},
"title": "المستند"
},
"lobe-web-browsing": {
"apiName": {
"crawlMultiPages": "قراءة محتوى عدة صفحات",
+10 -60
View File
@@ -12,6 +12,7 @@
"title": "معلومات المساعد"
},
"chat": {
"displayMode": "وضع العرض",
"enableHistoryCount": "تمكين عداد الرسائل السابقة",
"historyCount": "عدد الرسائل السابقة",
"no": "لا",
@@ -79,7 +80,6 @@
"group": {
"aiConfig": "إعدادات الذكاء الاصطناعي",
"common": "عام",
"market": "السوق",
"profile": "الحساب",
"system": "النظام"
},
@@ -223,82 +223,33 @@
"messages": {
"createVersionFailed": "فشل إنشاء الإصدار: {{message}}",
"fetchRemoteFailed": "فشل في جلب بيانات المساعد من السوق",
"missingIdentifier": "لا يحتوي هذا المساعد على معرف المجتمع حتى الآن",
"missingIdentifier": "المساعد الحالي لا يحتوي على معرف المجتمع",
"notAuthenticated": "يرجى تسجيل الدخول إلى حساب المجتمع أولاً",
"publishFailed": "فشل النشر: {{message}}"
},
"submitButton": "نشر",
"title": {
"submit": "مشاركة في مجتمع المساعدين",
"submit": "مشاركة في مجتمع المساعد",
"upload": "نشر إصدار جديد"
}
},
"resultModal": {
"message": "تم إرسال المساعد الذي أنشأته للمراجعة، وسيتم نشره تلقائيًا بعد الموافقة.",
"title": "تم الإرسال بنجاح",
"view": "عرض في المجتمع"
"view": "الانتقال إلى المجتمع لعرضه"
},
"submit": {
"button": "مشاركة في المجتمع",
"tooltip": "شارك المساعد في مجتمع المساعدين"
"tooltip": "شارك المساعد في المجتمع"
},
"upload": {
"button": "نشر إصدار جديد",
"tooltip": "نشر إصدار جديد في مجتمع المساعدين"
"tooltip": "نشر إصدار جديد في مجتمع المساعد"
}
},
"message": {
"success": "تم التحديث بنجاح"
},
"myAgents": {
"actions": {
"cancel": "إلغاء",
"confirmDeprecate": "تأكيد الإهمال",
"deprecate": "إهمال دائم",
"deprecateConfirmContent": "بعد الإهمال، سيتم إزالة هذا المساعد نهائيًا من السوق ولن يكون من الممكن إعادة نشره. هذا الإجراء لا يمكن التراجع عنه، يرجى الحذر.",
"deprecateConfirmTitle": "هل تريد تأكيد إهمال المساعد؟",
"deprecateError": "فشل في إهمال المساعد",
"deprecateLoading": "جارٍ إهمال المساعد...",
"deprecateSuccess": "تم إهمال المساعد",
"edit": "تحرير المساعد",
"publish": "نشر المساعد",
"publishError": "فشل في نشر المساعد",
"publishLoading": "جارٍ نشر المساعد...",
"publishSuccess": "تم نشر المساعد",
"unpublish": "إلغاء نشر المساعد",
"unpublishError": "فشل في إلغاء نشر المساعد",
"unpublishLoading": "جارٍ إلغاء نشر المساعد...",
"unpublishSuccess": "تم إلغاء نشر المساعد",
"viewDetail": "عرض التفاصيل"
},
"detail": {
"category": "الفئة",
"description": "الوصف",
"identifier": "المعرّف",
"title": "تفاصيل المساعد"
},
"empty": {
"description": "لم تقم بنشر أي مساعد في السوق بعد",
"title": "لا يوجد مساعدين منشورين"
},
"errors": {
"editFailed": "فشل في تحرير المساعد، يرجى المحاولة لاحقًا",
"fetchFailed": "فشل في جلب تفاصيل المساعد",
"notAuthenticated": "يرجى تسجيل الدخول إلى حساب السوق أولاً"
},
"loginRequired": {
"button": "تسجيل الدخول إلى حساب السوق",
"description": "يرجى تسجيل الدخول إلى حساب السوق لعرض المساعدين الذين قمت بنشرهم",
"title": "تسجيل الدخول مطلوب"
},
"status": {
"archived": "مؤرشف",
"deprecated": "مهمل",
"published": "منشور",
"unpublished": "غير منشور"
},
"title": "مساعدي المنشور"
},
"plugin": {
"addMCPPlugin": "إضافة مكون MCP",
"addTooltip": "إضافة البرنامج المساعد",
@@ -337,7 +288,7 @@
},
"submit": "تحديث معلومات المساعد",
"tag": {
"desc": "سيتم عرض وسم المساعد في مجتمع المساعدين",
"desc": "سيتم عرض وسم المساعد في مجتمع المساعد",
"placeholder": "الرجاء إدخال العلامة",
"title": "العلامة"
},
@@ -722,7 +673,7 @@
"metaMiss": "يرجى استكمال معلومات المساعد قبل التقديم، يجب أن تتضمن الاسم والوصف والعلامة",
"placeholder": "الرجاء إدخال معرف المساعد، يجب أن يكون فريدًا، مثل تطوير الويب",
"success": "تم إرسال المساعد بنجاح",
"tooltips": "مشاركة في مجتمع المساعدين"
"tooltips": "مشاركة في مجتمع المساعد"
},
"submitFooter": {
"reset": "إعادة تعيين",
@@ -817,13 +768,12 @@
"tab": {
"about": "حول",
"agent": "المساعد الافتراضي",
"apikey": "إدارة مفاتيح API",
"apikey": "إدارة مفتاح API",
"common": "المظهر",
"experiment": "تجربة",
"hotkey": "اختصارات لوحة المفاتيح",
"image": "خدمة الرسم",
"llm": "نموذج اللغة",
"my-agents": "مساعدي المنشور",
"profile": "حسابي",
"provider": "مزود خدمة الذكاء الاصطناعي",
"proxy": "وكيل الشبكة",
@@ -863,7 +813,7 @@
"verifyAuth": "لقد أكملت المصادقة"
},
"notInstalled": "غير مثبت",
"notInstalledWarning": "المكون الإضافي غير مثبت حاليًا، وقد يؤثر ذلك على استخدام المساعد",
"notInstalledWarning": "المكون الإضافي الحالي غير مثبت، وقد يؤثر ذلك على استخدام المساعد",
"plugins": {
"enabled": "ممكّنة {{num}}",
"groupName": "الإضافات",
-3
View File
@@ -54,7 +54,6 @@
},
"betterAuth": {
"errors": {
"confirmPasswordRequired": "Моля, потвърдете паролата",
"emailExists": "Този имейл вече е регистриран. Моля, влезте директно.",
"emailInvalid": "Моля, въведете валиден имейл адрес",
"emailNotRegistered": "Този имейл все още не е регистриран",
@@ -66,7 +65,6 @@
"passwordFormat": "Паролата трябва да съдържа както букви, така и цифри",
"passwordMaxLength": "Паролата не може да надвишава 64 знака",
"passwordMinLength": "Паролата трябва да бъде поне 8 знака",
"passwordMismatch": "Въведените пароли не съвпадат",
"passwordRequired": "Моля, въведете парола",
"usernameNotRegistered": "Това потребителско име не е регистрирано",
"usernameRequired": "Моля, въведете потребителско име"
@@ -127,7 +125,6 @@
"submit": "Вход"
},
"signup": {
"confirmPasswordPlaceholder": "Моля, потвърдете паролата",
"emailPlaceholder": "Моля, въведете имейл адрес",
"error": "Регистрацията не бе успешна, моля опитайте отново",
"firstNamePlaceholder": "Собствено име",
-16
View File
@@ -3,22 +3,6 @@
"title": "Модел"
},
"active": "Активен",
"agentBuilder": {
"installPlugin": {
"authRequired": "Облачното MCP разширение изисква удостоверяване",
"cancel": "Отказ",
"clickApproveToConnect": "Щракнете върху „Одобряване“, за да се свържете и упълномощите тази интеграция",
"connectedAndEnabled": "Свързано и активирано",
"connectionFailed": "Свързването не бе успешно",
"installFailed": "Инсталацията не бе успешна",
"installPlugin": "Инсталиране на разширение",
"installToEnable": "Инсталирайте това разширение, за да активирате помощника",
"installedAndEnabled": "Инсталирано и активирано",
"requiresAuth": "Изисква упълномощаване, щракнете върху „Одобряване“, за да се свържете",
"retry": "Опитай отново"
},
"welcome": "Здравей, аз съм **Lobe AI**, твоят експерт по конфигуриране на асистенти. Кажи ми какъв асистент искаш и ще ти помогна да го настроиш."
},
"agentDefaultMessage": "Здравейте, аз съм **{{name}}**, можете да започнете разговор с мен веднага или да отидете на [Настройки на асистента]({{url}}), за да попълните информацията ми.",
"agentDefaultMessageWithSystemRole": "Здравейте, аз съм **{{name}}**. Как мога да ви помогна?",
"agentDefaultMessageWithoutEdit": "Здравейте, аз съм **{{name}}**. Как мога да ви помогна?",
+1 -14
View File
@@ -58,7 +58,6 @@
"title": "История на версиите"
}
},
"downloads": "Изтегляния",
"list": "Списък с асистенти",
"marketSource": {
"label": "Превключване на източника на общността",
@@ -693,18 +692,6 @@
"home": "Начална страница",
"model": "Модел",
"plugin": "Плъгин",
"provider": "Доставчик на модели",
"user": "Потребител"
},
"user": {
"agents": "Асистенти",
"downloads": "Изтегляния",
"editProfile": "Редактиране на профил",
"login": "Вход",
"logout": "Изход",
"myProfile": "Моят профил",
"noAgents": "Този потребител все още не е публикувал асистенти",
"publishedAgents": "Публикувани асистенти",
"website": "Личен уебсайт"
"provider": "Доставчик на модели"
}
}
-14
View File
@@ -86,20 +86,6 @@
},
"newFolder": "Нова папка",
"newPage": "Създаване на нов документ",
"notion": {
"error": "Неуспешен импорт на файл от Notion",
"foundFiles": "Намерени {{count}} файла",
"importing": "Импортиране на файл от Notion...",
"noMarkdownFiles": "Не са намерени Markdown файлове в ZIP архива",
"partial": "Успешно импортирани {{success}} файла, неуспешни {{failed}}",
"success": "Успешно импортирани {{count}} файла"
},
"notionGuide": {
"cancel": "Отказ от импортиране",
"desc": "Моля, първо експортирайте Markdown (ZIP) от Notion. След това щракнете върху „Продължи“ и изберете архивния файл, за да импортирате всички страници.",
"ok": "Изберете Notion ZIP",
"title": "Импортиране на съдържание от Notion"
},
"uploadFile": "Качване на файл",
"uploadFolder": "Качване на папка"
},
-9
View File
@@ -1,9 +0,0 @@
{
"starter": {
"createAgent": "Създаване на асистент",
"deepResearch": "Задълбочено проучване",
"developing": "В процес на разработка",
"image": "Рисуване",
"write": "Писане"
}
}
+4 -4
View File
@@ -70,12 +70,12 @@
"title": "Бърза смяна на помощника"
},
"toggleLeftPanel": {
"desc": "Показване или скриване на левия панел",
"title": "Показване/Скриване на левия панел"
"desc": "Показване или скриване на панела с помощ отляво",
"title": "Показване/скриване на панела с помощника"
},
"toggleRightPanel": {
"desc": "Показване или скриване на десния панел",
"title": "Показване/Скриване на десния панел"
"desc": "Показване или скриване на панела с теми отдясно",
"title": "Показване/скриване на панела с теми"
},
"toggleZenMode": {
"desc": "В режим на фокус, показвайте само текущия разговор, скривайки другия интерфейс",
-37
View File
@@ -52,42 +52,5 @@
"submit": "Упълномощаването е успешно! Вече можете да публикувате помощник.",
"upload": "Упълномощаването е успешно! Вече можете да публикувате нова версия."
}
},
"profileSetup": {
"cancel": "Отказ",
"descriptionEdit": "Актуализирайте информацията във вашия профил в общността.",
"descriptionFirstTime": "Настройте своя профил, за да завършите създаването на профила в общността.",
"errors": {
"notAuthenticated": "Моля, влезте в системата, преди да продължите",
"updateFailed": "Неуспешно обновяване на профила. Моля, опитайте отново",
"usernameTaken": "Този потребителски ID вече е зает. Моля, изберете друг"
},
"fields": {
"description": {
"label": "Лично представяне",
"maxLength": "Личното представяне може да бъде до 200 знака",
"placeholder": "Разкажете ни малко за себе си..."
},
"displayName": {
"label": "Псевдоним",
"maxLength": "Псевдонимът може да бъде до 50 знака",
"placeholder": "Въведете вашия псевдоним",
"required": "Моля, въведете псевдоним"
},
"userName": {
"label": "Потребителски ID",
"maxLength": "Потребителският ID може да бъде до 32 знака",
"minLength": "Потребителският ID трябва да бъде поне 3 знака",
"pattern": "Потребителският ID може да съдържа само букви, цифри, долни черти и тирета",
"placeholder": "Въведете вашия потребителски ID",
"required": "Моля, въведете потребителски ID",
"tooltip": "Потребителският ID е вашият уникален идентификатор и ще се използва в линка към вашия профил"
}
},
"getStarted": "Започнете",
"save": "Запази",
"success": "Профилът е обновен успешно",
"titleEdit": "Редактиране на профил",
"titleFirstTime": "Завършете своя профил"
}
}
+1 -61
View File
@@ -1,38 +1,4 @@
{
"context": {
"actions": {
"delete": "Изтриване",
"edit": "Редактиране"
},
"defaultType": "Контекст",
"deleteConfirm": "Сигурни ли сте, че искате да изтриете тази контекстна памет? Това действие не може да бъде отменено.",
"deleteTitle": "Изтриване на контекстна памет",
"description": "Описание",
"empty": "Няма налична контекстна памет",
"impact": "Въздействие",
"source": "Източник",
"urgency": "Спешност"
},
"experience": {
"actions": {
"delete": "Изтриване",
"edit": "Редактиране"
},
"confidence": "Ниво на увереност",
"defaultType": "Опит",
"deleteConfirm": "Сигурни ли сте, че искате да изтриете тази памет за опит? Това действие не може да бъде отменено.",
"deleteTitle": "Изтриване на памет за опит",
"empty": "Няма налична памет за опит",
"keyLearning": "Ключово знание",
"situation": "Контекст",
"source": "Източник",
"steps": {
"action": "Предприето действие",
"outcome": "Възможен резултат",
"reasoning": "Разсъждение",
"situation": "Контекстуален фон"
}
},
"identity": {
"empty": "Няма запаметени самоличности",
"filter": {
@@ -61,31 +27,5 @@
"timeline": "Хронология"
}
},
"loading": "Зареждане...",
"preference": {
"actions": {
"delete": "Изтриване",
"edit": "Редактиране"
},
"conclusionDirectives": "Насоки за заключение",
"defaultType": "Предпочитание",
"deleteConfirm": "Сигурни ли сте, че искате да изтриете тази памет за предпочитание? Това действие не може да бъде отменено.",
"deleteTitle": "Изтриване на памет за предпочитание",
"empty": "Няма налична памет за предпочитание",
"priority": "Приоритет",
"source": "Източник",
"suggestions": "Препоръки"
},
"tab": {
"contexts": "Контексти",
"experiences": "Опит",
"home": "Начало",
"identities": "Идентичности",
"preferences": "Предпочитания",
"search": "Търсене"
},
"viewMode": {
"masonry": "Мозайка",
"timeline": "Хронология"
}
"loading": "Зареждане..."
}
-55
View File
@@ -1,25 +1,5 @@
{
"builtins": {
"lobe-agent-builder": {
"apiName": {
"getAvailableModels": "Извличане на налични модели",
"getAvailableTools": "Извличане на налични инструменти",
"getConfig": "Извличане на конфигурация",
"getMeta": "Извличане на метаданни",
"getPrompt": "Извличане на системен подкана",
"searchMarketTools": "Търсене в магазина за плъгини",
"searchOfficialTools": "Търсене на официални инструменти",
"setModel": "Задаване на модел",
"setOpeningMessage": "Задаване на начално съобщение",
"setOpeningQuestions": "Задаване на начални въпроси",
"togglePlugin": "Превключване на плъгин",
"updateChatConfig": "Актуализиране на конфигурацията на чата",
"updateConfig": "Актуализиране на конфигурацията",
"updateMeta": "Актуализиране на метаданните",
"updatePrompt": "Актуализиране на системния подкана"
},
"title": "Създател на агент"
},
"lobe-knowledge-base": {
"apiName": {
"readKnowledge": "Прочети съдържанието на базата знания",
@@ -44,41 +24,6 @@
},
"title": "Локална система"
},
"lobe-page-agent": {
"apiName": {
"batchUpdate": "Партидно актуализиране на възли",
"compareSnapshots": "Сравняване на моментни снимки",
"convertToList": "Преобразуване в списък",
"createNode": "Създаване на възел",
"cropImage": "Изрязване на изображение",
"deleteNode": "Изтриване на възел",
"deleteSnapshot": "Изтриване на моментна снимка",
"deleteTableColumn": "Изтриване на колона от таблица",
"deleteTableRow": "Изтриване на ред от таблица",
"duplicateNode": "Дублиране на възел",
"editTitle": "Редактиране на заглавието на документа",
"indentListItem": "Увеличаване на отстъпа на елемент от списък",
"initPage": "Инициализиране на документа",
"insertTableColumn": "Вмъкване на колона в таблица",
"insertTableRow": "Вмъкване на ред в таблица",
"listSnapshots": "Списък с моментни снимки",
"mergeNodes": "Сливане на възли",
"moveNode": "Преместване на възел",
"outdentListItem": "Намаляване на отстъпа на елемент от списък",
"replaceText": "Замяна на текст",
"resizeImage": "Преоразмеряване на изображение",
"restoreSnapshot": "Възстановяване на моментна снимка",
"rotateImage": "Завъртане на изображение",
"saveSnapshot": "Запазване на моментна снимка",
"setImageAlt": "Задаване на алтернативен текст на изображение",
"splitNode": "Разделяне на възел",
"toggleListType": "Превключване на тип списък",
"unwrapNode": "Разопаковане на възел",
"updateNode": "Актуализиране на възел",
"wrapNodes": "Опаковане на възли"
},
"title": "Документ"
},
"lobe-web-browsing": {
"apiName": {
"crawlMultiPages": "Прочети съдържание от няколко страници",
+11 -75
View File
@@ -12,6 +12,7 @@
"title": "Информация за асистента"
},
"chat": {
"displayMode": "Режим на показване",
"enableHistoryCount": "Разреши броене на историята",
"historyCount": "Брой съобщения в историята",
"no": "Не",
@@ -76,13 +77,6 @@
"title": "Нулиране на всички настройки"
}
},
"group": {
"aiConfig": "AI конфигурация",
"common": "Общи",
"market": "Пазар",
"profile": "Акаунт",
"system": "Система"
},
"groupTab": {
"chat": "Чат",
"members": "Членове",
@@ -223,7 +217,7 @@
"messages": {
"createVersionFailed": "Неуспешно създаване на версия: {{message}}",
"fetchRemoteFailed": "Неуспешно извличане на отдалечени данни за асистента",
"missingIdentifier": "Този асистент все още няма идентификатор в общността",
"missingIdentifier": "Текущият асистент все още няма идентификатор в общността",
"notAuthenticated": "Моля, първо влезте в акаунта си в общността",
"publishFailed": "Публикуването не бе успешно: {{message}}"
},
@@ -234,71 +228,22 @@
}
},
"resultModal": {
"message": "Вашият асистент е изпратен за преглед. След одобрение ще бъде публикуван автоматично.",
"title": "Успешно изпращане",
"message": "Вашият асистент е изпратен за преглед. След одобрение ще бъде автоматично публикуван.",
"title": "Успешно изпратено",
"view": "Виж в общността"
},
"submit": {
"button": "Сподели в общността",
"button": "Споделяне в общността",
"tooltip": "Споделете асистента в общността"
},
"upload": {
"button": "Публикувай нова версия",
"tooltip": "Публикувай нова версия в общността на асистенти"
"tooltip": "Публикуване на нова версия в общността на асистенти"
}
},
"message": {
"success": "Актуализацията беше успешна"
},
"myAgents": {
"actions": {
"cancel": "Отказ",
"confirmDeprecate": "Потвърди оттегляне",
"deprecate": "Постоянно оттегляне",
"deprecateConfirmContent": "След оттегляне, този агент ще бъде премахнат от пазара завинаги и няма да може да бъде публикуван отново. Това действие е необратимо, моля, действайте внимателно.",
"deprecateConfirmTitle": "Сигурни ли сте, че искате да оттеглите агента?",
"deprecateError": "Неуспешно оттегляне на агента",
"deprecateLoading": "Оттегляне на агента...",
"deprecateSuccess": "Агентът е оттеглен",
"edit": "Редактиране на агента",
"publish": "Публикуване на агента",
"publishError": "Неуспешно публикуване на агента",
"publishLoading": "Публикуване на агента...",
"publishSuccess": "Агентът е публикуван",
"unpublish": "Сваляне на агента",
"unpublishError": "Неуспешно сваляне на агента",
"unpublishLoading": "Сваляне на агента...",
"unpublishSuccess": "Агентът е свален",
"viewDetail": "Преглед на подробности"
},
"detail": {
"category": "Категория",
"description": "Описание",
"identifier": "Идентификатор",
"title": "Подробности за агента"
},
"empty": {
"description": "Все още не сте публикували агенти на пазара",
"title": "Няма публикувани агенти"
},
"errors": {
"editFailed": "Неуспешно редактиране на агента, моля опитайте отново по-късно",
"fetchFailed": "Неуспешно зареждане на подробности за агента",
"notAuthenticated": "Моля, влезте в акаунта си за пазара"
},
"loginRequired": {
"button": "Вход в акаунта за пазара",
"description": "Моля, влезте в акаунта си за пазара, за да видите публикуваните от вас агенти",
"title": "Необходим е вход"
},
"status": {
"archived": "Архивиран",
"deprecated": "Оттеглен",
"published": "Публикуван",
"unpublished": "Непубликуван"
},
"title": "Моите публикувани агенти"
},
"plugin": {
"addMCPPlugin": "Добавяне на MCP плъгин",
"addTooltip": "Персонализиран плъгин",
@@ -721,8 +666,8 @@
"identifier": "Идентификатор на асистента (identifier)",
"metaMiss": "Моля, попълнете информацията за агента, преди да го изпратите. Тя трябва да включва име, описание и тагове",
"placeholder": "Въведете уникален идентификатор за агента, напр. web-development",
"success": "Асистентът е изпратен успешно",
"tooltips": "Сподели в общността на асистенти"
"success": "Асистентът беше изпратен успешно",
"tooltips": "Споделяне в общността на асистенти"
},
"submitFooter": {
"reset": "Нулиране",
@@ -817,26 +762,19 @@
"tab": {
"about": "Относно",
"agent": "Агент по подразбиране",
"apikey": "Управление на API ключове",
"common": "Външен вид",
"common": "Общи настройки",
"experiment": "Експеримент",
"hotkey": "Бързи клавиши",
"image": "Услуга за рисуване",
"image": "AI рисуване",
"llm": "Езиков модел",
"my-agents": "Моите публикувани агенти",
"profile": "Моят акаунт",
"provider": "AI доставчик",
"proxy": "Мрежов прокси",
"security": "Сигурност",
"stats": "Статистика",
"storage": "Данни за хранилище",
"sync": "Синхронизиране в облака",
"system-agent": "Системен асистент",
"tts": "Текст към реч",
"usage": "Използване"
"tts": "Текст към реч"
},
"tools": {
"add": "Интегрирай плъгин",
"builtins": {
"groupName": "Вградени"
},
@@ -862,8 +800,6 @@
"tools": "инструмента",
"verifyAuth": "Удостоверяването е завършено"
},
"notInstalled": "Не е инсталиран",
"notInstalledWarning": "Този плъгин не е инсталиран и може да повлияе на работата на асистента",
"plugins": {
"enabled": "Активирани: {{num}}",
"groupName": "Плъгини",
-3
View File
@@ -54,7 +54,6 @@
},
"betterAuth": {
"errors": {
"confirmPasswordRequired": "Bitte bestätigen Sie Ihr Passwort",
"emailExists": "Diese E-Mail-Adresse ist bereits registriert. Bitte melden Sie sich direkt an.",
"emailInvalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"emailNotRegistered": "Diese E-Mail-Adresse ist noch nicht registriert",
@@ -66,7 +65,6 @@
"passwordFormat": "Das Passwort muss Buchstaben und Zahlen enthalten",
"passwordMaxLength": "Das Passwort darf maximal 64 Zeichen lang sein",
"passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein",
"passwordMismatch": "Die beiden eingegebenen Passwörter stimmen nicht überein",
"passwordRequired": "Bitte geben Sie ein Passwort ein",
"usernameNotRegistered": "Dieser Benutzername ist noch nicht registriert",
"usernameRequired": "Bitte geben Sie einen Benutzernamen ein"
@@ -127,7 +125,6 @@
"submit": "Anmelden"
},
"signup": {
"confirmPasswordPlaceholder": "Bitte bestätigen Sie Ihr Passwort",
"emailPlaceholder": "Bitte geben Sie Ihre E-Mail-Adresse ein",
"error": "Registrierung fehlgeschlagen, bitte versuchen Sie es erneut",
"firstNamePlaceholder": "Vorname",
-16
View File
@@ -3,22 +3,6 @@
"title": "Modell"
},
"active": "Aktiv",
"agentBuilder": {
"installPlugin": {
"authRequired": "Das Cloud-MCP-Plugin erfordert eine Anmeldung",
"cancel": "Abbrechen",
"clickApproveToConnect": "Klicken Sie auf „Genehmigen“, um die Verbindung herzustellen und diese Integration zu autorisieren",
"connectedAndEnabled": "Verbunden und aktiviert",
"connectionFailed": "Verbindung fehlgeschlagen",
"installFailed": "Installation fehlgeschlagen",
"installPlugin": "Plugin installieren",
"installToEnable": "Installieren Sie dieses Plugin, um den Assistenten zu aktivieren",
"installedAndEnabled": "Installiert und aktiviert",
"requiresAuth": "Autorisierung erforderlich, klicken Sie auf „Genehmigen“, um die Verbindung herzustellen",
"retry": "Erneut versuchen"
},
"welcome": "Hallo, ich bin **Lobe AI**, dein Experte für die Konfiguration von Assistenten. Sag mir, was für einen Assistenten du dir wünschst, und ich helfe dir, ihn einzurichten."
},
"agentDefaultMessage": "Hallo, ich bin **{{name}}**. Du kannst sofort mit mir sprechen oder zu den [Assistenteneinstellungen]({{url}}) gehen, um meine Informationen zu vervollständigen.",
"agentDefaultMessageWithSystemRole": "Hallo, ich bin **{{name}}**. Wie kann ich Ihnen behilflich sein?",
"agentDefaultMessageWithoutEdit": "Hallo, ich bin **{{name}}**. Wie kann ich Ihnen behilflich sein?",
+1 -14
View File
@@ -58,7 +58,6 @@
"title": "Versionsverlauf"
}
},
"downloads": "Downloads",
"list": "Assistentenliste",
"marketSource": {
"label": "Community-Quelle wechseln",
@@ -693,18 +692,6 @@
"home": "Startseite",
"model": "Modell",
"plugin": "Plugin",
"provider": "Modellanbieter",
"user": "Benutzer"
},
"user": {
"agents": "Assistenten",
"downloads": "Downloads",
"editProfile": "Profil bearbeiten",
"login": "Anmelden",
"logout": "Abmelden",
"myProfile": "Mein Profil",
"noAgents": "Dieser Benutzer hat noch keine Assistenten veröffentlicht",
"publishedAgents": "Veröffentlichte Assistenten",
"website": "Persönliche Webseite"
"provider": "Modellanbieter"
}
}
-14
View File
@@ -86,20 +86,6 @@
},
"newFolder": "Neuen Ordner erstellen",
"newPage": "Neues Dokument",
"notion": {
"error": "Fehler beim Importieren der Notion-Datei",
"foundFiles": "{{count}} Dateien gefunden",
"importing": "Notion-Dateien werden importiert...",
"noMarkdownFiles": "Keine Markdown-Dateien in der ZIP-Datei gefunden",
"partial": "{{success}} Dateien erfolgreich importiert, {{failed}} fehlgeschlagen",
"success": "{{count}} Dateien erfolgreich importiert"
},
"notionGuide": {
"cancel": "Jetzt nicht importieren",
"desc": "Bitte exportiere zunächst Markdown (ZIP) aus Notion. Klicke dann auf „Weiter“, um das ZIP-Archiv auszuwählen und alle Seiten zu importieren.",
"ok": "Notion-ZIP auswählen",
"title": "Notion-Inhalte importieren"
},
"uploadFile": "Datei hochladen",
"uploadFolder": "Ordner hochladen"
},
-9
View File
@@ -1,9 +0,0 @@
{
"starter": {
"createAgent": "Assistent erstellen",
"deepResearch": "Tiefenrecherche",
"developing": "In Entwicklung",
"image": "Zeichnen",
"write": "Schreiben"
}
}
+4 -4
View File
@@ -70,12 +70,12 @@
"title": "Schnell zwischen Assistenten wechseln"
},
"toggleLeftPanel": {
"desc": "Linke Seitenleiste ein- oder ausblenden",
"title": "Linke Seitenleiste ein-/ausblenden"
"desc": "Linkes Hilfepanel ein- oder ausblenden",
"title": "Assistentenpanel ein-/ausblenden"
},
"toggleRightPanel": {
"desc": "Rechte Seitenleiste ein- oder ausblenden",
"title": "Rechte Seitenleiste ein-/ausblenden"
"desc": "Rechtes Themenpanel ein- oder ausblenden",
"title": "Themenpanel ein-/ausblenden"
},
"toggleZenMode": {
"desc": "Im Fokusmodus nur die aktuelle Sitzung anzeigen, andere UI ausblenden",
-37
View File
@@ -52,42 +52,5 @@
"submit": "Autorisierung erfolgreich! Du kannst jetzt einen Assistenten veröffentlichen.",
"upload": "Autorisierung erfolgreich! Du kannst jetzt eine neue Version veröffentlichen."
}
},
"profileSetup": {
"cancel": "Abbrechen",
"descriptionEdit": "Aktualisiere deine Community-Profilinformationen.",
"descriptionFirstTime": "Richte dein Profil ein, um dein Community-Konto zu vervollständigen.",
"errors": {
"notAuthenticated": "Bitte melde dich an, um fortzufahren.",
"updateFailed": "Profilaktualisierung fehlgeschlagen. Bitte versuche es erneut.",
"usernameTaken": "Diese Benutzer-ID ist bereits vergeben. Bitte wähle eine andere."
},
"fields": {
"description": {
"label": "Über mich",
"maxLength": "Die Beschreibung darf maximal 200 Zeichen enthalten.",
"placeholder": "Erzähle etwas über dich..."
},
"displayName": {
"label": "Spitzname",
"maxLength": "Der Spitzname darf maximal 50 Zeichen enthalten.",
"placeholder": "Gib deinen Spitznamen ein",
"required": "Bitte gib einen Spitznamen ein"
},
"userName": {
"label": "Benutzer-ID",
"maxLength": "Die Benutzer-ID darf maximal 32 Zeichen enthalten.",
"minLength": "Die Benutzer-ID muss mindestens 3 Zeichen lang sein.",
"pattern": "Die Benutzer-ID darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.",
"placeholder": "Gib deine Benutzer-ID ein",
"required": "Bitte gib eine Benutzer-ID ein",
"tooltip": "Die Benutzer-ID ist dein eindeutiger Identifikator und wird in deinem Profil-Link verwendet."
}
},
"getStarted": "Loslegen",
"save": "Speichern",
"success": "Profil erfolgreich aktualisiert",
"titleEdit": "Profil bearbeiten",
"titleFirstTime": "Vervollständige dein Profil"
}
}
+1 -61
View File
@@ -1,38 +1,4 @@
{
"context": {
"actions": {
"delete": "Löschen",
"edit": "Bearbeiten"
},
"defaultType": "Kontext",
"deleteConfirm": "Möchten Sie diesen Kontextspeicher wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"deleteTitle": "Kontextspeicher löschen",
"description": "Beschreibung",
"empty": "Keine Kontextspeicher vorhanden",
"impact": "Auswirkungsgrad",
"source": "Quelle",
"urgency": "Dringlichkeit"
},
"experience": {
"actions": {
"delete": "Löschen",
"edit": "Bearbeiten"
},
"confidence": "Vertrauensgrad",
"defaultType": "Erfahrung",
"deleteConfirm": "Möchten Sie diesen Erfahrungsspeicher wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"deleteTitle": "Erfahrungsspeicher löschen",
"empty": "Keine Erfahrungsspeicher vorhanden",
"keyLearning": "Zentrale Erkenntnis",
"situation": "Kontext",
"source": "Quelle",
"steps": {
"action": "Maßnahme",
"outcome": "Mögliches Ergebnis",
"reasoning": "Begründung",
"situation": "Situationshintergrund"
}
},
"identity": {
"empty": "Keine Identitätserinnerung vorhanden",
"filter": {
@@ -61,31 +27,5 @@
"timeline": "Zeitleiste"
}
},
"loading": "Wird geladen...",
"preference": {
"actions": {
"delete": "Löschen",
"edit": "Bearbeiten"
},
"conclusionDirectives": "Schlussfolgerungsanweisungen",
"defaultType": "Präferenz",
"deleteConfirm": "Möchten Sie diesen Präferenzspeicher wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"deleteTitle": "Präferenzspeicher löschen",
"empty": "Keine Präferenzspeicher vorhanden",
"priority": "Priorität",
"source": "Quelle",
"suggestions": "Vorschläge"
},
"tab": {
"contexts": "Kontexte",
"experiences": "Erfahrungen",
"home": "Startseite",
"identities": "Identitäten",
"preferences": "Präferenzen",
"search": "Suche"
},
"viewMode": {
"masonry": "Kachelansicht",
"timeline": "Zeitstrahl"
}
"loading": "Wird geladen..."
}
-55
View File
@@ -1,25 +1,5 @@
{
"builtins": {
"lobe-agent-builder": {
"apiName": {
"getAvailableModels": "Verfügbare Modelle abrufen",
"getAvailableTools": "Verfügbare Werkzeuge abrufen",
"getConfig": "Konfiguration abrufen",
"getMeta": "Metadaten abrufen",
"getPrompt": "System-Prompt abrufen",
"searchMarketTools": "Plugin-Marktplatz durchsuchen",
"searchOfficialTools": "Offizielle Werkzeuge durchsuchen",
"setModel": "Modell festlegen",
"setOpeningMessage": "Begrüßungsnachricht festlegen",
"setOpeningQuestions": "Einstiegsfragen festlegen",
"togglePlugin": "Plugin umschalten",
"updateChatConfig": "Chat-Konfiguration aktualisieren",
"updateConfig": "Konfiguration aktualisieren",
"updateMeta": "Metadaten aktualisieren",
"updatePrompt": "System-Prompt aktualisieren"
},
"title": "Agenten-Builder"
},
"lobe-knowledge-base": {
"apiName": {
"readKnowledge": "Wissensdatenbank lesen",
@@ -44,41 +24,6 @@
},
"title": "Lokales System"
},
"lobe-page-agent": {
"apiName": {
"batchUpdate": "Knoten stapelweise aktualisieren",
"compareSnapshots": "Schnappschüsse vergleichen",
"convertToList": "In Liste umwandeln",
"createNode": "Knoten erstellen",
"cropImage": "Bild zuschneiden",
"deleteNode": "Knoten löschen",
"deleteSnapshot": "Schnappschuss löschen",
"deleteTableColumn": "Tabellenspalte löschen",
"deleteTableRow": "Tabellenzeile löschen",
"duplicateNode": "Knoten duplizieren",
"editTitle": "Dokumenttitel bearbeiten",
"indentListItem": "Listenelement einrücken",
"initPage": "Dokument initialisieren",
"insertTableColumn": "Tabellenspalte einfügen",
"insertTableRow": "Tabellenzeile einfügen",
"listSnapshots": "Schnappschüsse auflisten",
"mergeNodes": "Knoten zusammenführen",
"moveNode": "Knoten verschieben",
"outdentListItem": "Einrückung des Listenelements entfernen",
"replaceText": "Text ersetzen",
"resizeImage": "Bildgröße ändern",
"restoreSnapshot": "Schnappschuss wiederherstellen",
"rotateImage": "Bild drehen",
"saveSnapshot": "Schnappschuss speichern",
"setImageAlt": "Alternativtext für Bild festlegen",
"splitNode": "Knoten aufteilen",
"toggleListType": "Listentyp umschalten",
"unwrapNode": "Knoten entpacken",
"updateNode": "Knoten aktualisieren",
"wrapNodes": "Knoten umschließen"
},
"title": "Dokument"
},
"lobe-web-browsing": {
"apiName": {
"crawlMultiPages": "Inhalte mehrerer Seiten lesen",
+14 -81
View File
@@ -2,7 +2,6 @@
"about": {
"title": "Über"
},
"advancedSettings": "Erweiterte Einstellungen",
"agentInfoDescription": {
"basic": {
"avatar": "Profilbild",
@@ -12,6 +11,7 @@
"title": "Assistenteninformationen"
},
"chat": {
"displayMode": "Anzeigemodus",
"enableHistoryCount": "Anzahl vergangener Nachrichten aktivieren",
"historyCount": "Anzahl vergangener Nachrichten",
"no": "Nein",
@@ -76,13 +76,6 @@
"title": "Alle Einstellungen zurücksetzen"
}
},
"group": {
"aiConfig": "KI-Konfiguration",
"common": "Allgemein",
"market": "Marktplatz",
"profile": "Konto",
"system": "System"
},
"groupTab": {
"chat": "Chat",
"members": "Mitglieder",
@@ -223,82 +216,32 @@
"messages": {
"createVersionFailed": "Versionserstellung fehlgeschlagen: {{message}}",
"fetchRemoteFailed": "Fehler beim Abrufen der entfernten Assistentendaten",
"missingIdentifier": "Dieser Assistent hat noch keine Community-Kennung",
"notAuthenticated": "Bitte melden Sie sich zuerst mit Ihrem Community-Konto an",
"missingIdentifier": "Der aktuelle Assistent hat noch keinen Markt-Bezeichner",
"notAuthenticated": "Bitte melden Sie sich zuerst mit Ihrem Marktkonto an",
"publishFailed": "Veröffentlichung fehlgeschlagen: {{message}}"
},
"submitButton": "Veröffentlichen",
"title": {
"submit": "In der Assistenten-Community teilen",
"submit": "Im Assistenten-Markt teilen",
"upload": "Neue Version veröffentlichen"
}
},
"resultModal": {
"message": "Ihr erstellter Assistent wurde zur Überprüfung eingereicht und wird nach erfolgreicher Prüfung automatisch veröffentlicht.",
"title": "Erfolgreich eingereicht",
"view": "In der Community ansehen"
"message": "Der Assistent wurde zur Überprüfung eingereicht. Nach erfolgreicher Prüfung wird er automatisch veröffentlicht. Klicken Sie auf „Im Markt ansehen“, um den veröffentlichten Assistenten zu sehen.",
"view": "Im Markt ansehen"
},
"submit": {
"button": "In der Community teilen",
"tooltip": "Assistent in der Community veröffentlichen"
"button": "Im Markt teilen",
"tooltip": "Assistent im Markt veröffentlichen"
},
"upload": {
"button": "Neue Version veröffentlichen",
"tooltip": "Neue Version in der Assistenten-Community veröffentlichen"
"tooltip": "Neue Version im Assistenten-Markt veröffentlichen"
}
},
"message": {
"success": "Erfolgreich aktualisiert"
},
"myAgents": {
"actions": {
"cancel": "Abbrechen",
"confirmDeprecate": "Veraltete Version bestätigen",
"deprecate": "Dauerhaft entfernen",
"deprecateConfirmContent": "Nach dem Entfernen wird dieser Assistent dauerhaft aus dem Marktplatz entfernt und kann nicht erneut veröffentlicht werden. Dieser Vorgang ist unwiderruflich. Bitte seien Sie vorsichtig.",
"deprecateConfirmTitle": "Assistent wirklich entfernen?",
"deprecateError": "Assistent konnte nicht entfernt werden",
"deprecateLoading": "Assistent wird entfernt...",
"deprecateSuccess": "Assistent wurde entfernt",
"edit": "Assistent bearbeiten",
"publish": "Assistent veröffentlichen",
"publishError": "Veröffentlichung des Assistenten fehlgeschlagen",
"publishLoading": "Assistent wird veröffentlicht...",
"publishSuccess": "Assistent wurde veröffentlicht",
"unpublish": "Assistent zurückziehen",
"unpublishError": "Zurückziehen des Assistenten fehlgeschlagen",
"unpublishLoading": "Assistent wird zurückgezogen...",
"unpublishSuccess": "Assistent wurde zurückgezogen",
"viewDetail": "Details anzeigen"
},
"detail": {
"category": "Kategorie",
"description": "Beschreibung",
"identifier": "Bezeichner",
"title": "Assistenten-Details"
},
"empty": {
"description": "Du hast noch keinen Assistenten im Marktplatz veröffentlicht",
"title": "Keine veröffentlichten Assistenten"
},
"errors": {
"editFailed": "Bearbeiten des Assistenten fehlgeschlagen. Bitte versuche es später erneut.",
"fetchFailed": "Details des Assistenten konnten nicht geladen werden",
"notAuthenticated": "Bitte melde dich zuerst mit deinem Marktplatz-Konto an"
},
"loginRequired": {
"button": "Im Marktplatz anmelden",
"description": "Bitte melde dich mit deinem Marktplatz-Konto an, um deine veröffentlichten Assistenten zu sehen",
"title": "Anmeldung erforderlich"
},
"status": {
"archived": "Archiviert",
"deprecated": "Veraltet",
"published": "Veröffentlicht",
"unpublished": "Nicht veröffentlicht"
},
"title": "Meine veröffentlichten Assistenten"
},
"plugin": {
"addMCPPlugin": "MCP-Plugin hinzufügen",
"addTooltip": "Benutzerdefiniertes Plugin",
@@ -337,7 +280,7 @@
},
"submit": "Assistenteninformationen aktualisieren",
"tag": {
"desc": "Die Tags des Assistenten werden in der Assistenten-Community angezeigt",
"desc": "Die Assistenten-Tags werden im Assistentenmarkt angezeigt",
"placeholder": "Bitte geben Sie ein Tag ein",
"title": "Tag"
},
@@ -721,8 +664,7 @@
"identifier": "Assistenten-Bezeichner (identifier)",
"metaMiss": "Bitte vervollständigen Sie die Assistenteninformationen, einschließlich Name, Beschreibung und Tags, bevor Sie sie einreichen.",
"placeholder": "Geben Sie die Kennung des Assistenten ein, die eindeutig sein muss, z. B. Web-Entwicklung",
"success": "Assistent erfolgreich eingereicht",
"tooltips": "In der Assistenten-Community teilen"
"tooltips": "Auf dem Assistentenmarkt teilen"
},
"submitFooter": {
"reset": "Zurücksetzen",
@@ -817,26 +759,19 @@
"tab": {
"about": "Über",
"agent": "Standard-Assistent",
"apikey": "API-Schlüsselverwaltung",
"common": "Erscheinungsbild",
"common": "Allgemeine Einstellungen",
"experiment": "Experiment",
"hotkey": "Tastenkombinationen",
"image": "Bildgenerierungsdienst",
"image": "AI-Zeichnung",
"llm": "Sprachmodell",
"my-agents": "Meine veröffentlichten Assistenten",
"profile": "Mein Konto",
"provider": "KI-Dienstanbieter",
"proxy": "Netzwerkproxy",
"security": "Sicherheit",
"stats": "Statistiken",
"storage": "Datenspeicher",
"sync": "Cloud-Synchronisierung",
"system-agent": "Systemassistent",
"tts": "Sprachdienste",
"usage": "Nutzungsstatistik"
"tts": "Sprachdienste"
},
"tools": {
"add": "Plugin integrieren",
"builtins": {
"groupName": "Integriert"
},
@@ -862,8 +797,6 @@
"tools": "Tools",
"verifyAuth": "Ich habe die Authentifizierung abgeschlossen"
},
"notInstalled": "Nicht installiert",
"notInstalledWarning": "Dieses Plugin ist derzeit nicht installiert und könnte die Nutzung des Assistenten beeinträchtigen",
"plugins": {
"enabled": "Aktiviert: {{num}}",
"groupName": "Plugins",
-3
View File
@@ -54,7 +54,6 @@
},
"betterAuth": {
"errors": {
"confirmPasswordRequired": "Please confirm your password",
"emailExists": "This email is already registered. Please sign in instead",
"emailInvalid": "Please enter a valid email address or username",
"emailNotRegistered": "This email or username is not registered",
@@ -66,7 +65,6 @@
"passwordFormat": "Password must contain both letters and numbers",
"passwordMaxLength": "Password must not exceed 64 characters",
"passwordMinLength": "Password must be at least 8 characters",
"passwordMismatch": "The passwords do not match",
"passwordRequired": "Please enter your password",
"usernameNotRegistered": "This username is not registered",
"usernameRequired": "Please enter your username"
@@ -127,7 +125,6 @@
"submit": "Sign In"
},
"signup": {
"confirmPasswordPlaceholder": "Confirm your password",
"emailPlaceholder": "Enter your email address",
"error": "Sign up failed, please try again",
"firstNamePlaceholder": "First Name",
-23
View File
@@ -3,22 +3,6 @@
"title": "Model"
},
"active": "Active",
"agentBuilder": {
"installPlugin": {
"authRequired": "Authentication required for cloud MCP plugins",
"cancel": "Cancel",
"clickApproveToConnect": "Click \"Approve\" to connect and authorize this integration",
"connectedAndEnabled": "Connected and enabled",
"connectionFailed": "Connection failed",
"installFailed": "Installation failed",
"installPlugin": "Install Plugin",
"installToEnable": "Install this plugin to enable it for the agent",
"installedAndEnabled": "Installed and enabled",
"requiresAuth": "Requires authorization. Click \"Approve\" to connect",
"retry": "Retry"
},
"welcome": "Hello, I'm **Lobe AI**, your assistant configuration expert. Tell me what kind of assistant you want, and I'll set it up for you."
},
"agentDefaultMessage": "Hello, I am **{{name}}**. You can start a conversation with me right away, or you can go to [Assistant Settings]({{url}}) to complete my information.",
"agentDefaultMessageWithSystemRole": "Hello, I am **{{name}}**. How can I assist you today?",
"agentDefaultMessageWithoutEdit": "Hello, I am **{{name}}**. How can I assist you today?",
@@ -106,13 +90,8 @@
},
"groupDescription": "Team Description",
"groupSidebar": {
"agentProfile": {
"chat": "Chat",
"model": "Model"
},
"members": {
"addMember": "Add Member",
"enableOrchestrator": "Enable Host",
"memberSettings": "Member Settings",
"orchestrator": "Host",
"orchestratorThinking": "The host is thinking...",
@@ -385,7 +364,6 @@
}
},
"tab": {
"groupProfile": "Group Profile",
"profile": "Assistant Profile",
"search": "Search"
},
@@ -453,7 +431,6 @@
"clear": "Clear Speech"
},
"untitledAgent": "Untitled Assistant",
"untitledGroup": "Untitled Group",
"updateAgent": "Update Assistant Information",
"upload": {
"action": {

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