Compare commits

...

261 Commits

Author SHA1 Message Date
kangfenmao
115f111071 chore(version): 0.8.20 2024-12-03 12:52:08 +08:00
kangfenmao
a4d1bcffd9 style: update api key input field formatting and validation 2024-12-03 12:24:25 +08:00
kangfenmao
f5d37a4e53 feat: implement automatic text translation functionality 2024-12-03 11:55:52 +08:00
kangfenmao
a9d4a0885c feat: add MessageErrorBoundary component 2024-12-03 11:15:35 +08:00
kangfenmao
6596497c97 feat: added topic auto-renaming to messages page 2024-12-03 10:57:38 +08:00
kangfenmao
12d8f57dab feat: add enable topic naming settings #399 2024-12-03 10:47:43 +08:00
kangfenmao
7f2f3ad88a refactor: shortcuts feature 2024-12-03 10:23:53 +08:00
kangfenmao
cd3c053f81 feat: add translations and reset functionality for new features 2024-12-02 22:29:18 +08:00
kangfenmao
7dacd58821 feat: add shortcut feature 2024-12-02 18:03:38 +08:00
kangfenmao
744a6ac7cb feat: add translated message content 2024-12-02 13:41:28 +08:00
kangfenmao
2e9041c891 feat: auto translate input text 2024-11-30 22:37:33 +08:00
kangfenmao
3717ff25bf feat: generate random seed for image generation in usepaintings hook 2024-11-30 20:22:07 +08:00
kangfenmao
494d52ac85 fix: update ipc handle to retrieve update information 2024-11-30 20:22:00 +08:00
kangfenmao
22d2ff1518 feat: added command shortcuts for new topic input 2024-11-30 14:18:38 +08:00
kangfenmao
06ae4328ea style: update code block padding and topic position 2024-11-30 12:01:00 +08:00
kangfenmao
8de1197557 feat: add configurable request options to gemini provider 2024-11-26 13:15:25 +08:00
kangfenmao
09e86b35a5 fix: proxy 2024-11-25 15:57:16 +08:00
kangfenmao
76ea170a01 chore(version): 0.8.19 2024-11-25 14:16:33 +08:00
kangfenmao
4cd962b42f fix: 链接失败警告不消失 #382
close #382
2024-11-25 14:14:03 +08:00
kangfenmao
1cae86f93d fix: 模型回答时可以新建对话 #369
close #369
2024-11-25 14:10:09 +08:00
kangfenmao
1171100417 fix: code block copy button missing 2024-11-25 13:56:41 +08:00
kangfenmao
dcf57651fe feat: update input component and add translation features 2024-11-25 13:43:03 +08:00
kangfenmao
603b867a5f fix: auto update assistant model 2024-11-25 12:49:47 +08:00
kangfenmao
e765bf9828 feat: create assistant with search text 2024-11-25 12:30:46 +08:00
kangfenmao
33d5da7325 feat: clear app cache 2024-11-25 11:16:24 +08:00
kangfenmao
383e8255a0 feat: add localization text and model type support #378 2024-11-25 10:31:57 +08:00
kangfenmao
4cb5c128bb feat: add border to list item 2024-11-24 01:35:41 +08:00
kangfenmao
949a13b021 fix: 设置中有两个“代码风格”项 #373 2024-11-23 23:08:27 +08:00
kangfenmao
e6c9cb60dc chore(version): 0.8.18 2024-11-23 14:23:22 +08:00
kangfenmao
ad625b23a7 feat: improve proxy configuration handling 2024-11-23 14:21:08 +08:00
kangfenmao
2e34b79f26 build: add scripts/cloudflare-worker.js 2024-11-22 10:01:54 +08:00
kangfenmao
9288e7b292 feat: update electron configuration and add locale-based language selection #370 2024-11-21 18:14:27 +08:00
kangfenmao
ec703852f8 style: adjust window vibrancy style 2024-11-20 20:18:51 +08:00
kangfenmao
fca9fb0c84 chore(version): 0.8.17 2024-11-20 19:15:33 +08:00
kangfenmao
64c8831530 style: removed dark theme styles, updated search and ui 2024-11-20 19:14:09 +08:00
kangfenmao
75e396ecf0 chore(version): 0.8.16 2024-11-20 18:50:27 +08:00
kangfenmao
697c3b1838 feat: implement new ui features and styles 2024-11-20 17:49:39 +08:00
kangfenmao
efa0f4cbdb Revert "feat: remove model settings from settings tab"
This reverts commit a9aa5a8da0.

# Conflicts:
#	src/renderer/src/pages/home/Tabs/Settings.tsx
2024-11-20 17:38:10 +08:00
kangfenmao
c3414e9b6d feat: add new model visionallowedmodels 2024-11-20 16:40:00 +08:00
kangfenmao
6ba6108f43 style: update styles for improved visual consistency and layout 2024-11-20 16:37:52 +08:00
kangfenmao
c3d007b52c feat: added size attribute to preview and download buttons 2024-11-20 11:57:57 +08:00
kangfenmao
e1494d408f feat: enable auto-update and auto-install of app updates 2024-11-20 11:04:42 +08:00
kangfenmao
cd625430b2 style: adjusted layout and styling for searchpopup component 2024-11-20 11:01:02 +08:00
kangfenmao
aefb08965d chore(version): 0.8.15 2024-11-19 23:46:22 +08:00
kangfenmao
d2dd70000b docs: visual updates to shortcutsettings page 2024-11-19 20:53:12 +08:00
kangfenmao
f0a96bb34c fix: 无法恢复备份文件 #341 2024-11-19 20:30:47 +08:00
kangfenmao
0ec61e1c47 feat: new settings ui 2024-11-19 19:54:18 +08:00
kangfenmao
335ce4963b feat: added data folder 2024-11-19 18:48:57 +08:00
kangfenmao
63ef0d2df1 fix: 编辑消息无效 #313 2024-11-19 15:34:03 +08:00
kangfenmao
c0c0e8ae33 style: update antdesign segmented styles 2024-11-19 15:17:54 +08:00
kangfenmao
771b078df9 feat: adjusted searchpopup dimensions and added assistant generation checks 2024-11-19 15:08:04 +08:00
kangfenmao
64e70ea918 style: update color scheme to darker shades of black 2024-11-19 14:45:22 +08:00
kangfenmao
2d46a4494e feat: add search popup 2024-11-19 14:17:38 +08:00
kangfenmao
9b7e2282fe feat: improved agents page filtering and layout adjustments, added russian translations support 2024-11-19 12:04:01 +08:00
kangfenmao
535b7d0a92 style: update code block ui styling and logic 2024-11-19 11:49:35 +08:00
kangfenmao
223496192d feat: validate html elements in markdown, refactor cursor type, disable markdown input rendering 2024-11-19 11:45:00 +08:00
kangfenmao
db779446f0 chore(version): 0.8.14 2024-11-19 11:13:38 +08:00
kangfenmao
8ef3ef2a8f fix: has unsafe elements 2024-11-19 11:13:16 +08:00
Emmanuel Ferdman
30da183578 docs: update license reference
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-11-19 09:19:02 +08:00
首都爱护动物协会
49c09f381c Update models.ts
douhbao matching rules update
2024-11-19 04:08:26 +08:00
kangfenmao
c8c58ddcfb feat: auto switch to topics 2024-11-18 17:51:17 +08:00
kangfenmao
5bffb86d4f feat: code collapsible settings 2024-11-18 17:44:33 +08:00
kangfenmao
84fa5b065b wip 2024-11-18 17:16:55 +08:00
kangfenmao
7ecb35dfa7 fix: 缩放快捷键不支持小键盘 #354 2024-11-18 17:05:50 +08:00
kangfenmao
2d7d403b15 fix: 重启之后无法保存界面的缩放 #354 2024-11-18 17:00:26 +08:00
kangfenmao
7342a0afef feat: added proxy settings and handling functionality 2024-11-18 14:05:17 +08:00
kangfenmao
1b8a3885f7 feat: add provider type 2024-11-18 13:04:46 +08:00
kangfenmao
c33c0b20f2 feat: enabled provider after add model 2024-11-18 11:41:17 +08:00
kangfenmao
4f75f29361 feat: added gemini experimental to vision allowed models 2024-11-18 11:26:02 +08:00
injurka
fe00eed7b9 feat: russia locale 2024-11-13 08:21:13 +08:00
kangfenmao
30fa9277ff chore: update release workflow with tag filtering 2024-11-12 19:59:44 +08:00
kangfenmao
11a446e106 chore(version): 0.8.13 2024-11-12 19:27:46 +08:00
kangfenmao
d2ca6f1041 feat: add new button to inputbar component and remove unnecessary toolbar button 2024-11-12 19:27:46 +08:00
kangfenmao
0f3dc87d08 fix: improved popup item filtering logic 2024-11-12 19:27:46 +08:00
kangfenmao
7d3cae1f5b feat: add adaptive tray icon handling 2024-11-12 17:52:00 +08:00
kangfenmao
ceae1fa3d0 feat: remove tray behavior on app close, add platform detection and file extensions 2024-11-12 16:36:47 +08:00
kangfenmao
12a2c8c86d feat: added api connection validation and provider configuration 2024-11-12 13:56:03 +08:00
kangfenmao
29d6c4be18 feat: update system_models to use gpt-4o-latest model 2024-11-12 13:28:40 +08:00
kangfenmao
738e51c078 style: adjusted styling and layout for mac os and file list view 2024-11-12 12:09:34 +08:00
kangfenmao
db050c002a feat: improved pin model functionality and translations 2024-11-12 11:54:11 +08:00
kangfenmao
398f995cd1 feat: handle sorted and filtered agent groups in agentspage 2024-11-12 11:26:09 +08:00
kangfenmao
348fc365fa feat: update system tray icon functionality and ui 2024-11-12 11:16:55 +08:00
kangfenmao
7b6d38e349 feat: add provider deletion functionality and related translations 2024-11-12 10:14:01 +08:00
kangfenmao
6a35c0e3d8 fix: fixed tiling window manager and password input handling issues 2024-11-12 10:00:58 +08:00
kangfenmao
9a63169a73 fix: 视觉模型识别错误 #329 2024-11-12 09:23:24 +08:00
kangfenmao
a9aa5a8da0 feat: remove model settings from settings tab 2024-11-12 00:50:13 +08:00
kangfenmao
a2d568175b feat: update translations and display settings 2024-11-12 00:37:23 +08:00
kangfenmao
0b9717780d feat: improved tray functionality and ui theme settings 2024-11-12 00:25:57 +08:00
kangfenmao
b371fed814 style: improved mermaid popup styling and positioning 2024-11-11 23:49:44 +08:00
injurka
3311f8cdef feat: added settings for minimize to tray instead of closing 2024-11-11 23:49:18 +08:00
kangfenmao
422baa848b fix: fix select box issue with mermaid diagrams 2024-11-11 23:33:57 +08:00
kangfenmao
739aa21475 style: improved chat window styling and accessibility 2024-11-11 23:26:45 +08:00
kangfenmao
23ef4ab952 fix: adjusted tray icon size to match linux platform requirements 2024-11-11 18:03:03 +08:00
injurka
b77f845cb0 fix: the context menu is not displayed on linux 2024-11-11 17:58:20 +08:00
kangfenmao
0573b274ed feat: add loading indicator and disable check button while checking api keys 2024-11-11 17:57:48 +08:00
kangfenmao
60433bb1ab style: update bubble ui component styles 2024-11-11 17:41:53 +08:00
kangfenmao
1caf53fbda fix: message color 2024-11-11 13:40:25 +08:00
kangfenmao
446e011c6a chore(version): 0.8.12 2024-11-11 11:18:43 +08:00
kangfenmao
c319b54a26 feat: rename images to paintings for consistency 2024-11-11 10:45:42 +08:00
kangfenmao
e9ca1d54a0 feat: add check all keys popup 2024-11-11 10:41:43 +08:00
kangfenmao
1ff8fe0c2e fix: removed macos window minimize event handler 2024-11-11 09:25:22 +08:00
kangfenmao
902341bc1d chore(version): 0.8.11 2024-11-10 17:20:52 +08:00
kangfenmao
17fff46024 style: update code formatting for markdown styles 2024-11-10 17:16:10 +08:00
kangfenmao
d258c1cfe2 feat: add mermaid preview and download feature 2024-11-10 17:09:18 +08:00
kangfenmao
79cabadfb8 fix: 加入账号轮询功能 #293 2024-11-09 23:18:56 +08:00
kangfenmao
61ceca2363 feat: update sidebar menu with interactive tooltips and painting terminology 2024-11-09 22:26:35 +08:00
kangfenmao
8a8deda002 fix: removed theme dependency in syntaxhighlighterprovider, added mermaid delay 2024-11-09 15:02:38 +08:00
kangfenmao
6536ec227a fix: 气泡默认界面显示异常 #308 2024-11-09 14:54:22 +08:00
kangfenmao
af1fd90118 feat: add mermaid library and theme initialization 2024-11-09 14:51:39 +08:00
kangfenmao
68fa2bad15 fix: mermaid图渲染有问题 #310 2024-11-09 14:16:42 +08:00
kangfenmao
bac3bad8db style: improved syntax highlighting functionality and code loading 2024-11-09 10:05:34 +08:00
kangfenmao
e11633310c feat: add the ability to display the application in tray #297 2024-11-09 08:42:09 +08:00
kangfenmao
612b39a878 fix: code style init value 2024-11-08 11:59:32 +08:00
injurka
8491141edc fix: expand code syntax highlighting options (#307)
* added locale for context-menu

* fix: expand code syntax highlighting options

* fix: type for theme

---------

Co-authored-by: injurka <ikornilov.ext@prosebya.ru~>
2024-11-08 11:59:05 +08:00
kangfenmao
088628f89f chore(version): 0.8.10 2024-11-07 23:06:42 +08:00
kangfenmao
a6b4e48640 fix: 添加自定义的模型的分组总是成为大写字母 #257 2024-11-07 22:51:37 +08:00
kangfenmao
ba0e2c5848 fix: input token i18n 2024-11-07 22:51:37 +08:00
injurka
d986087857 fix: when changing the values in the inputs in the tab Default Assisting, they do not change (#305)
* added locale for context-menu

* fix: when changing the values in the inputs in the tab Default Assisting, they do not change

---------

Co-authored-by: injurka <ikornilov.ext@prosebya.ru~>
2024-11-07 22:13:47 +08:00
kangfenmao
73c93c5581 feat: 添加DuckDuckGo AI Chat #271 2024-11-07 22:08:11 +08:00
kangfenmao
ceec4a9f97 fix: 删除最后一组预设消息组后无法保存 #300 2024-11-07 18:11:45 +08:00
kangfenmao
cf08467552 fix: 气泡模式,代码框配色看不清楚 #302 2024-11-07 17:49:31 +08:00
kangfenmao
34c85e8f0c fix: github models 不支持图片 #291 2024-11-07 17:32:37 +08:00
injurka
1db3faa2a8 fix type import 2024-11-06 22:09:28 +08:00
injurka
35efada37e added locale for context-menu 2024-11-06 22:09:28 +08:00
kangfenmao
ceca3408ff refactor: refactored capturescrollablediv function and updated chat and messages components 2024-11-06 15:56:48 +08:00
kangfenmao
f2def559d4 chore: remove unused llm store model and migration config 2024-11-06 13:17:43 +08:00
首都爱护动物协会
cd97be0f10 Match the avatar for the Pixtral model. 2024-11-05 20:50:43 +08:00
首都爱护动物协会
b87394ed88 add pixtral avatar 2024-11-05 20:50:43 +08:00
kangfenmao
5d01d12d2a fix: fix shortcut registration and unregistration 2024-11-05 17:25:38 +08:00
Hypered1
d7f4e4584a Fix typo in zh-cn.json 2024-11-05 16:48:56 +08:00
首都爱护动物协会
eda870f181 Add new model and provider avatars. 2024-11-05 16:48:07 +08:00
首都爱护动物协会
3f093a91be Add Providers
1. Fix the naming error of the Grok model
2. Add new providers: Grok, Mistral, Jina, Hyperbolic
2024-11-05 16:48:07 +08:00
kangfenmao
45dd76e281 chore(version): 0.8.9 2024-11-03 01:26:23 +08:00
kangfenmao
568d4814e3 feat: remove image compress 2024-11-03 01:26:00 +08:00
kangfenmao
9468f3b511 refactor: main code 2024-11-02 23:32:59 +08:00
kangfenmao
04af940144 feat: export to word 2024-11-02 21:45:23 +08:00
kangfenmao
e33d9ac0ae fix: 在对话中插入图片时,应该自动压缩一下 #132 2024-11-02 15:30:41 +08:00
kangfenmao
cd835b7c36 fix: Ctrl + - 缩放时 缩到最小 再缩的话会报错 #266 2024-11-02 15:13:43 +08:00
kangfenmao
dd4239da87 fix: 默认模型在模型服务中禁用后显示错误 #266 2024-11-02 15:11:36 +08:00
kangfenmao
41c3895da4 feat: add message card style switch 2024-11-01 21:50:40 +08:00
kangfenmao
2e9c7d0830 fix: chat text color 2024-11-01 18:14:23 +08:00
kangfenmao
8ea73e14c9 style(message): user message use black color 2024-11-01 17:43:21 +08:00
kangfenmao
3791556b13 chore(version): 0.8.8 2024-11-01 12:39:11 +08:00
kangfenmao
e0dab5cf5b feat: set topic position to left 2024-11-01 12:28:13 +08:00
kangfenmao
1785e7df0a feat: dynamic handling for tab groups 2024-11-01 12:18:48 +08:00
kangfenmao
6cb1846b23 fix: ui and layout 2024-11-01 12:06:30 +08:00
kangfenmao
21243579b3 feat: added sorting functionality and updated translations 2024-11-01 11:46:11 +08:00
kangfenmao
0d2ad2e4c3 feat: improved ui layout and added reusable add agent card component 2024-11-01 11:00:17 +08:00
kangfenmao
071a3950cd feat: topic position set default to left 2024-10-31 17:11:32 +08:00
Teo
dc6066b74c refactor(navbar): 移除未使用的代码 2024-10-31 16:51:38 +08:00
Teo
ce55d8d0e7 refactor(navbar): 移除未使用的代码 2024-10-31 16:51:38 +08:00
Teo
d4ae321cd2 style(toggleTheme): 将主题切换按钮移到左下角菜单栏中 2024-10-31 16:51:38 +08:00
Teo
89dd35c98d style(chat): 对话界面改为左右布局 2024-10-31 16:51:38 +08:00
Teo
b8c70a3061 style(markdown): 小代码块不换行 2024-10-31 16:51:38 +08:00
kangfenmao
968a749aaa chore(version): 0.8.7 2024-10-31 15:01:37 +08:00
kangfenmao
e2fc593624 style: update responsive container styling for paintingslist component 2024-10-31 14:39:27 +08:00
kangfenmao
0e1674ce6c feat: added new painting functionality with mac device restriction 2024-10-31 14:35:05 +08:00
kangfenmao
18566989be chore(version): 0.8.6 2024-10-31 13:34:06 +08:00
kangfenmao
31fa10f185 feat: improved real-time painting generation support 2024-10-31 13:28:07 +08:00
kangfenmao
f6aa0dc55a feat: added translations and ui improvements 2024-10-31 13:18:35 +08:00
kangfenmao
ca2a9ed84a feat: added translation options for opening all files 2024-10-31 12:17:27 +08:00
kangfenmao
79f6d598ab feat: added translation functionality and chinese support 2024-10-31 12:11:30 +08:00
kangfenmao
fb564733e4 style: prevent drag on image preview switch 2024-10-31 11:38:37 +08:00
kangfenmao
63e5972dd2 feat: improvedscrollbar component functionality and added internationalization support to agentspage component 2024-10-31 11:31:38 +08:00
首都爱护动物协会
b80270709f add new providers logo 2024-10-31 09:37:17 +08:00
首都爱护动物协会
d7b459dcee Agents Page Upgrade
1. Simplified the layout of the agents page for improved user experience.

2. Enhanced the design of agent cards for a more visually appealing look.
2024-10-31 09:37:17 +08:00
kangfenmao
76b9e1a65e fix: painting no provider 2024-10-31 09:23:58 +08:00
kangfenmao
b148c5adf5 feat: files ui improvements 2024-10-30 20:45:48 +08:00
kangfenmao
2313f66ad9 refactor: services 2024-10-30 17:23:52 +08:00
kangfenmao
02edd983d1 chore: remove useless files 2024-10-30 00:32:27 +08:00
kangfenmao
3e049baaa4 feat: added file download functionality and improved api 2024-10-30 00:32:27 +08:00
kangfenmao
7401d85825 feat: add paintaing page 2024-10-30 00:32:27 +08:00
AHpx-Lap
241dcddfed feat: use auto theme as default 2024-10-30 00:31:56 +08:00
kangfenmao
cd0ea8154d chore: update dependencies and electron version 2024-10-30 00:31:03 +08:00
kangfenmao
6d6788eeb2 feat: update feature list and documentation for improved user understanding 2024-10-29 16:12:16 +08:00
kangfenmao
9ac35ae3d8 docs: update documentation to reflect project changes 2024-10-29 15:29:44 +08:00
kangfenmao
72e847258d feat: add instance lock and second instance handling 2024-10-29 14:48:48 +08:00
kangfenmao
0cc460a4a3 chore(version): 0.8.5 2024-10-29 02:46:45 +08:00
kangfenmao
98307d5d85 feat: add chinese translations and improve ui 2024-10-29 02:26:10 +08:00
kangfenmao
f73749ac63 feat: add keyborad shortcut settings 2024-10-29 01:55:11 +08:00
kangfenmao
c5deba270f docs: update sponsorship links and qr code references 2024-10-29 00:45:26 +08:00
kangfenmao
bf5617393b feat: enhanced search functionality with translation support 2024-10-29 00:40:44 +08:00
kangfenmao
057efbf98c fix: agents sort 2024-10-29 00:27:35 +08:00
kangfenmao
2143a6614e fix: add X-Api-Key headers #246 2024-10-28 23:33:20 +08:00
kangfenmao
6f9eb2ae75 fix: add claude-3-5-sonnet-latest support #247 2024-10-28 16:40:37 +08:00
kangfenmao
73c2945961 fix: agents tabs not shown 2024-10-28 16:23:55 +08:00
首都爱护动物协会
18beffcc29 Update agents.json 2024-10-28 08:50:59 +08:00
kangfenmao
2b17319855 feat: enhanced text wrapping and ant-input styling 2024-10-27 22:50:45 +08:00
kangfenmao
d77c1ce2b4 feat: update agents.json 2024-10-27 19:53:20 +08:00
kangfenmao
b43f5c9ead fix: fix stale state issue in chat component 2024-10-27 19:30:18 +08:00
kangfenmao
a8651ec558 feat: enhanced ui with translation and layout improvements 2024-10-27 19:13:54 +08:00
kangfenmao
d76a173706 feat: use real file path 2024-10-27 18:58:23 +08:00
kangfenmao
7ec3cb05f2 feat: scroll to bottom on messages page load 2024-10-27 18:33:01 +08:00
kangfenmao
a83d514169 fix: removed filter condition and messages from fetchchatcompletion() payload 2024-10-27 00:11:30 +08:00
kangfenmao
1f8551135f style: optimized performance and refined styles 2024-10-26 23:48:14 +08:00
kangfenmao
1444739cc6 style: align tab content horizontally and ignore agents.json with prettier 2024-10-26 23:36:06 +08:00
kangfenmao
c7cbecad68 feat: update ui components with improved design and functionality 2024-10-26 23:14:33 +08:00
kangfenmao
ab1c597e1c refactor: improved code readability for filtering agents 2024-10-26 22:38:31 +08:00
kangfenmao
ac21c90b6f feat: add agents tabs and search 2024-10-26 22:33:47 +08:00
kangfenmao
9ec0836d26 feat: added icons to buttons for preview and download 2024-10-26 17:29:35 +08:00
kangfenmao
ee966010e1 refactor: messages completion 2024-10-26 17:12:06 +08:00
kangfenmao
7c99621558 fix: WebDAV 备份失败 maxBodyLength 限制 #243 2024-10-25 13:26:46 +08:00
kangfenmao
cfb3eb7d90 docs: update documentation for a more inclusive environment and added japanese and chinese documentation 2024-10-25 00:09:01 +08:00
kangfenmao
64ad2fc9f4 chore(version): 0.8.4 2024-10-24 23:14:14 +08:00
kangfenmao
7f0909c796 fix: 新的滚动条组件 2024-10-24 23:08:11 +08:00
kangfenmao
27631d9cff chore(version): 0.8.3 2024-10-24 18:47:20 +08:00
kangfenmao
596cf8e3f2 docs: update translation and api url tip 2024-10-24 16:19:47 +08:00
kangfenmao
6e2ab66b81 fix: 添加默认助手会添加两个 #238 2024-10-24 15:46:08 +08:00
kangfenmao
2cbb4c8831 fix: 公式显示问题 #239 2024-10-24 15:33:04 +08:00
kangfenmao
5347f63aa8 fix: 添加默认助手会添加两个 #238 2024-10-24 15:05:13 +08:00
kangfenmao
077a66c675 feat: scrollbar 2024-10-24 14:58:13 +08:00
kangfenmao
6e7b6d8387 build: update yarn.lock 2024-10-24 11:45:41 +08:00
Ikko Eltociear Ashimine
bdf6df1936 docs: add Japanese README file
I created Japanese translated README.
2024-10-23 20:50:58 +08:00
kangfenmao
a2dd440f77 fix: 公式又不居中了 #231 2024-10-23 20:49:22 +08:00
kangfenmao
b47d6c95e7 fix: 修复数据库和 store 数据不一致问题 2024-10-23 14:08:48 +08:00
kangfenmao
6265d27ebc feat: add cherry-stuido-db project 2024-10-22 22:01:56 +08:00
kangfenmao
0dd60cb129 chore(version): 0.8.2 2024-10-22 20:06:11 +08:00
kangfenmao
04dae10d89 fix: 汉语新解卡片 css 2024-10-22 19:53:59 +08:00
kangfenmao
71ef0f319f feat: 话题分享功能 #103 2024-10-22 19:01:46 +08:00
kangfenmao
58817ae82f fix: 文件保存相关问题 #208 2024-10-22 17:37:22 +08:00
kangfenmao
7e477cb9c7 docs: CONTRIBUTING.md.md to CONTRIBUTING.md 2024-10-22 16:04:31 +08:00
kangfenmao
1063610c01 fix: scrollbar width 2024-10-22 16:03:13 +08:00
kangfenmao
927670d3a3 build: add pulish:artifacts command 2024-10-22 15:43:28 +08:00
XuQing Chai
2fea7659b1 Modify the path of README.zh.md in README.md correctly 2024-10-21 23:27:18 +08:00
kangfenmao
43b9298329 feat: 智能体改进:名称、上下文支持、模型参数支持 #59 2024-10-21 23:21:46 +08:00
kangfenmao
fe2e3bfc36 feat: add qwen2-vl modal vision support 2024-10-18 13:21:11 +08:00
牡丹凤凰
bae80fda8d Merge pull request #205 from cawabj/develop
misc
2024-10-18 03:31:22 +08:00
首都爱护动物协会
ab709b9c61 misc 2024-10-18 03:30:05 +08:00
kangfenmao
2c28e3bb76 style: improved layout and functionality for the prompt editing field. 2024-10-17 16:52:18 +08:00
kangfenmao
d98020e12c docs: update readme documentation with telegram link and welcome message 2024-10-17 16:29:47 +08:00
kangfenmao
25addc390f docs: update community information and telegram 2024-10-17 16:22:19 +08:00
kangfenmao
88d04a1a6e docs: add product hunt 2024-10-17 15:59:41 +08:00
kangfenmao
1f582c672d fix: remove some file extensions 2024-10-17 15:13:18 +08:00
kangfenmao
c913b2a6d0 fix: java 格式文件上传支持 #201
close #201
2024-10-17 14:26:08 +08:00
kangfenmao
267c60f24d docs: update LICENSE 2024-10-17 14:09:30 +08:00
kangfenmao
a8ccaf6847 feat: Agents 页面改版 #198 2024-10-17 13:44:52 +08:00
kangfenmao
a3a005b946 docs: update commercial use license terms 2024-10-17 13:36:20 +08:00
kangfenmao
2220a6016e feat: added human-readable file size formatting and unit support 2024-10-17 13:35:51 +08:00
kangfenmao
3197390f1a docs: update references to main branch 2024-10-17 10:06:21 +08:00
kangfenmao
5f04d1adb1 fix: local package.json 2024-10-16 17:43:19 +08:00
kangfenmao
76b6593545 fix: 检查更新按钮不生效 #184
close #184
2024-10-16 13:14:15 +08:00
kangfenmao
04ce641bf7 fix: removed unnecessary newline replacement
- Removed unnecessary newline replacement from input message content.
2024-10-16 11:23:58 +08:00
kangfenmao
31e912aac3 fix: 点击清除上下文直接跳转到最下面 #192
close #192
2024-10-16 09:53:56 +08:00
kangfenmao
832ec99d92 chore: removed resources from excluded files
- Removed resources from excluded files.
2024-10-16 09:44:29 +08:00
kangfenmao
ef9fda6d0c chore(version): 0.8.1 2024-10-15 21:19:54 +08:00
kangfenmao
624230411a feat: added new translations and api url handling features
- Added new translation strings for API URL actions and hints.
- Updated Chinese translations and added new provider API URL descriptions.
- Added new translations for API URL preview and reset tip.
- Added support for Open AI API settings preview and hint.
- Added a new isOpenAIProvider function to handle specific provider type checks.
- Added a new function to validate if a given URL has a valid non-root path.
2024-10-15 21:14:19 +08:00
kangfenmao
14808649f8 feat: added conditional rendering to messagetokens component
- Added conditional rendering to MessageTokens component.
- Added parameter 'isLastMessage' to MessageTokens component to determine conditional rendering based on message position.
2024-10-15 20:22:01 +08:00
kangfenmao
3cc8cfb43b fix: code font size 2024-10-15 19:21:18 +08:00
kangfenmao
4055111ade feat: add show line number in code 2024-10-15 19:18:12 +08:00
kangfenmao
dc98b27e3e feat: add license.html 2024-10-15 19:02:53 +08:00
kangfenmao
90fec317e5 feat: add data settings 2024-10-15 18:56:09 +08:00
kangfenmao
303a0e20a0 fix: webdav备份恢复的逻辑似乎有点问题 #178 2024-10-15 17:48:48 +08:00
kangfenmao
d69252a7da feat: add success message on new branch creation
- Added new functionality to emit success message upon creating a new branch.
2024-10-15 17:13:41 +08:00
kangfenmao
99f05383cb feat: update translations and add new topic functionality 2024-10-15 16:33:15 +08:00
kangfenmao
5ba6c9f882 feat: add default timestamps for topic updates
- Added default values for createdAt and updatedAt timestamps when updating topics.
2024-10-15 16:19:38 +08:00
kangfenmao
27f64409d6 feat: added drag and drop file upload feature #190
- Added drag and drop file uploading functionality to input bar.

close #190
2024-10-15 16:15:59 +08:00
kangfenmao
7237729ff6 feat: enhanced model search in popup
- Improved search functionality for selecting models in the popup by modifying the filter criteria to include both model and provider names.
2024-10-15 15:55:15 +08:00
kangfenmao
d29cd3c657 feat: improved data display and scrolling experience
- Increased file list pagination size to improve data display.
- Disable inline styles for Markdown content.
- Removed overflow functionality for a smoother scrolling experience.
2024-10-15 15:15:58 +08:00
kangfenmao
8c87f59822 docs: update readme 2024-10-15 15:15:45 +08:00
1355873789
5780141df4 新增:腾讯混元服务商 2024-10-15 01:58:17 +08:00
kangfenmao
f5799ef47b fix: 一次上传多个文件 #183
close #183
2024-10-14 22:52:35 +08:00
225 changed files with 23919 additions and 4627 deletions

View File

@@ -2,4 +2,4 @@ node_modules
dist dist
out out
.gitignore .gitignore
scripts/cloudflare-worker.js

View File

@@ -1,6 +1,12 @@
name: Release name: Release
on: on:
workflow_dispatch:
inputs:
version:
description: 'Version (e.g. v1.2.3)'
required: true
type: string
push: push:
tags: tags:
- v*.*.* - v*.*.*
@@ -71,5 +77,6 @@ jobs:
dist/*.rpm dist/*.rpm
dist/*.tar.gz dist/*.tar.gz
dist/latest*.yml dist/latest*.yml
dist/*.blockmap
env: env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

6
.gitignore vendored
View File

@@ -19,12 +19,6 @@ lerna-debug.log*
*.sln *.sln
*.sw? *.sw?
# NPM
npm/*/*
!npm/*/dist
!npm/*/package.json
!npm/*/*.js
# Yarn # Yarn
.pnp.* .pnp.*
.yarn/* .yarn/*

View File

@@ -5,3 +5,4 @@ LICENSE.md
tsconfig.json tsconfig.json
tsconfig.*.json tsconfig.*.json
CHANGELOG*.md CHANGELOG*.md
agents.json

View File

@@ -0,0 +1,26 @@
diff --git a/core.js b/core.js
index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d82573ea5 100644
--- a/core.js
+++ b/core.js
@@ -156,7 +156,7 @@ class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
...this.authHeaders(opts),
};
}
diff --git a/core.mjs b/core.mjs
index 8bc7a0ee10d61560d7113cf3f703355bb19f7ddd..5e4c8586ea6b13fe887a22af2de05eaa4700b5ec 100644
--- a/core.mjs
+++ b/core.mjs
@@ -149,7 +149,7 @@ export class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
...this.authHeaders(opts),
};
}

29
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,29 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

45
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,45 @@
# Cherry Studio 贡献者指南
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
## 如何贡献
以下是您可以参与的几种方式:
1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。
2. **修复 BUG**:如果您发现了 BUG欢迎提交修复方案。请在提交前确认问题已被解决并附上相关测试。
3. **维护 Issue**:协助我们管理 GitHub 上的 issue帮助标记、分类和解决问题。
4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。
5. **编写文档**帮助我们完善用户手册、API 文档和开发者指南。
6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。
7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio吸引更多用户和开发者。
## 开始贡献
1. **Fork 仓库**:在 GitHub 上 fork 我们的仓库,并将其克隆到本地。
2. **创建分支**:为您要进行的更改创建一个新的分支。
3. **提交更改**:在本地进行更改并提交。请确保您的提交信息清晰明了。
4. **发起 Pull Request**:将您的更改推送到 GitHub并发起 Pull Request。请描述您的更改内容和原因。
### 其他建议
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。
## 联系我们
如果您有任何问题或建议,欢迎通过以下方式联系我们:
- 微信kangfenmao
- [GitHub Issues](https://github.com/kangfenmao/cherry-studio/issues)
感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。

136
LICENSE
View File

@@ -1,101 +1,79 @@
### Cherry Studio 商业许可协议 ## Cherry Studio 用户协议
欢迎使用 Cherry Studio 桌面 AI 客户端工具。请仔细阅读以下协议条款,继续使用本软件即表示您同意本协议内容。
**许可协议**
本软件采用 Apache License 2.0 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry Studio 时还应遵守以下附加条款:
**一. 商用许可**
1. **免费商用**:用户在不修改代码的情况下,可以免费用于商业目的。
2. **商业授权**:如果您满足以下任意条件之一,需取得商业授权:
1. 对本软件进行二次修改、开发包括但不限于修改应用名称、logo、代码以及功能
2. 为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。
3. 预装或集成到硬件设备或产品中进行捆绑销售。
4. 政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
**二. 贡献者协议**
作为 Cherry Studio 的贡献者,您应当同意以下条款:
1. **许可调整**:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。
2. **商业用途**:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。
**三. 其他条款**
1. 本协议条款的解释权归 Cherry Studio 开发者所有。
2. 本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。
如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问 http://www.apache.org/licenses/LICENSE-2.0。
--- ---
#### 中文版 根据 Apache 许可证 2.0 版(“许可证”)进行许可;除非符合许可证,否则您不得使用此文件。您可以在以下网址获取许可证副本:
**Cherry Studio 商业许可协议** http://www.apache.org/licenses/LICENSE-2.0
本协议(以下简称“协议”)由以下双方签订: 除非适用法律要求或书面同意,软件根据许可证分发的内容以“原样”分发,不附带任何明示或暗示的保证或条件。请参阅特定语言管理权限的许可证和许可证下的限制。
- 许可方王谦kangfenmao@qq.com ## Cherry Studio User Agreement
- 被许可方:[被许可方名称]
**1. 定义** Welcome to Cherry Studio, a desktop AI client tool. Please read the following agreement carefully. By continuing to use this software, you agree to the terms outlined below.
- “软件”指 Cherry Studio 软件,网址为 https://cherry-ai.com。 **License Agreement**
- “商业用途”指任何以盈利为目的的使用。
**2. 许可** This software is licensed under the **Apache License 2.0**. In addition to the terms of the Apache License 2.0, the following additional terms apply to the use of Cherry Studio:
- 未经许可方明确书面许可,被许可方不得将软件用于商业用途。 **I. Commercial Use License**
- 未经许可方事先书面同意,被许可方不得将软件全部或部分用于商业用途分发。
- 未经许可方明确授权,被许可方不得再许可、租赁、销售、出租或以其他方式将软件转让给任何第三方用于商业用途。
**3. 责任限制** 1. **Free Commercial Use**: Users can use the software for commercial purposes without modifying the code.
2. **Commercial License Required**: A commercial license is required if any of the following conditions are met:
1. You modify, develop, or alter the software, including but not limited to changes to the application name, logo, code, or functionality.
2. You provide multi-tenant services to enterprise customers with 10 or more users.
3. You pre-install or integrate the software into hardware devices or products and bundle it for sale.
4. You are engaging in large-scale procurement for government or educational institutions, especially involving security, data privacy, or other sensitive requirements.
开发者不对因使用本软件而产生的任何直接或间接损失承担责任。用户应自行承担使用本软件的风险。 **II. Contributor Agreement**
**4. 许可协议生效日期** As a contributor to Cherry Studio, you agree to the following:
本许可协议自用户首次下载或使用本软件之日起生效。 1. **License Adjustment**: The producer reserves the right to adjust the open-source license as needed, making it stricter or more lenient.
2. **Commercial Use**: Any code you contribute may be used for commercial purposes, including but not limited to cloud business operations.
**5. 许可终止** **III. Other Terms**
如发现用户违反上述条款,开发者有权随时终止本许可,并要求用户停止使用本软件及删除所有相关副本。 1. The interpretation of these terms is subject to the discretion of Cherry Studio developers.
2. These terms may be updated, and users will be notified through the software when changes occur.
**6. 其他** For any questions or to request a commercial license, please contact the Cherry Studio development team.
本协议的解释、效力及争议的解决,均适用中华人民共和国法律。 Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
**7. 联系信息**
- 许可方联系方式:
- 手机号18539907620
- 邮箱kangfenmao@qq.com
**许可方(签字):**
**日期:**
**被许可方(签字):**
**日期:**
--- ---
#### English Version Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
**Cherry Studio Commercial License Agreement** Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
This Agreement ("Agreement") is entered into by and between:
- Licensor: Wang Qian (kangfenmao)
- Licensee: [Licensee Name]
**1. Definitions**
- "Software" refers to the Cherry Studio software, available at https://cherry-ai.com.
- "Commercial Use" refers to any use for profit.
**2. License**
- The Licensee may not use the Software for Commercial Use without the Licensor's explicit written permission.
- The Licensee may not distribute the Software in whole or in part for Commercial Use without the Licensor's prior written consent.
- The Licensee may not sublicense, lease, sell, rent, or otherwise transfer the Software to any third party for Commercial Use without the Licensor's explicit authorization.
**3. Termination of License**
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
**4. Effective Date of License Agreement**
This license agreement becomes effective from the date the user first downloads or uses the software.
**5. Termination of License**
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
**6. Miscellaneous**
This Agreement shall be governed by and construed in accordance with the laws of the People's Republic of China.
**7. Contact Information**
- Licensor's Contact Details:
- Phone: 18539907620
- Email: kangfenmao@qq.com
**Licensor (Signature):**
**Date:**
**Licensee (Signature):**
**Date:**

View File

@@ -1,35 +1,67 @@
<div align="center"> <div align="center">
<a href="https://github.com/kangfenmao/cherry-studio/releases"> <a href="https://github.com/kangfenmao/cherry-studio/releases">
<img src="https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505" alt="banner" /> <img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
</a> </a>
English | <a href="./docs/README.zh.md">中文</a> </div>
<div align="center">
English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a>
</div> </div>
# 🍒 Cherry Studio # 🍒 Cherry Studio
![](https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505)
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux. Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
👏 Join [Telegram Group](https://t.me/CherryStudioAI)
# 🌠 Screenshot # 🌠 Screenshot
![](https://github.com/user-attachments/assets/28585d83-4bf0-4714-b561-8c7bf57cc600) ![](https://github.com/user-attachments/assets/28585d83-4bf0-4714-b561-8c7bf57cc600)
![](https://github.com/user-attachments/assets/8576863a-f632-4776-bc12-657eeced9da3) ![](https://github.com/user-attachments/assets/8576863a-f632-4776-bc12-657eeced9da3)
![](https://github.com/user-attachments/assets/790790d7-b462-48dd-bde1-91c1697a4648) ![](https://github.com/user-attachments/assets/790790d7-b462-48dd-bde1-91c1697a4648)
# 🌟 Features # 🌟 Key Features
1. Support for Multiple LLM Providers. 1. **Diverse LLM Provider Support**:
2. Allows creation of multiple Assistants.
3. Enables creation of multiple topics. - ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
4. Allows using multiple models to answer questions in the same conversation. - 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others
5. Supports drag-and-drop sorting. - 💻 Local Model Support with Ollama
6. Code highlighting.
7. Mermaid chart 2. **AI Assistants & Conversations**:
- 📚 300+ Pre-configured AI Assistants
- 🤖 Custom Assistant Creation
- 💬 Multi-model Simultaneous Conversations
3. **Document & Data Processing**:
- 📄 Support for Text, Images, Office, PDF, and more
- ☁️ WebDAV File Management and Backup
- 📊 Mermaid Chart Visualization
- 💻 Code Syntax Highlighting
4. **Practical Tools Integration**:
- 🔍 Global Search Functionality
- 📝 Topic Management System
- 🔤 AI-powered Translation
- 🎯 Drag-and-drop Sorting
- 🔌 Mini Program Support
5. **Enhanced User Experience**:
- 🖥️ Cross-platform Support for Windows, Mac, and Linux
- 📦 Ready to Use, No Environment Setup Required
- 🎨 Light/Dark Themes and Transparent Window
- 📝 Complete Markdown Rendering
- 🤲 Easy Content Sharing
# 🖥️ Develop # 🖥️ Develop
## Recommended IDE Setup ## IDE Setup
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) [Cursor](https://www.cursor.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## Project Setup ## Project Setup
@@ -58,20 +90,52 @@ $ yarn build:mac
$ yarn build:linux $ yarn build:linux
``` ```
# ⭐️ Star History # 🤝 Contributing
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline) We welcome contributions to Cherry Studio! Here are some ways you can contribute:
1. **Contribute Code**: Develop new features or optimize existing code.
2. **Fix Bugs**: Submit fixes for any bugs you find.
3. **Maintain Issues**: Help manage GitHub issues.
4. **Product Design**: Participate in design discussions.
5. **Write Documentation**: Improve user manuals and guides.
6. **Community Engagement**: Join discussions and help users.
7. **Promote Usage**: Spread the word about Cherry Studio.
## Getting Started
1. **Fork the Repository**: Fork and clone it to your local machine.
2. **Create a Branch**: For your changes.
3. **Submit Changes**: Commit and push your changes.
4. **Open a Pull Request**: Describe your changes and reasons.
For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIBUTING.md).
Thank you for your support and contributions!
# 🚀 Contributors # 🚀 Contributors
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors"> <a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" /> <img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
</a> </a>
<br /><br />
# Sponsor # 🌐 Community
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
# 📣 Product Hunt
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
# ☕ Sponsor
[Buy Me a Coffee](docs/sponsor.md) [Buy Me a Coffee](docs/sponsor.md)
# 📃 License # 📃 License
[LICENSE](./LICENSE) [LICENSE](./LICENSE)
# ⭐️ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)

BIN
build/tray_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
build/tray_icon_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
build/tray_icon_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

140
docs/README.ja.md Normal file
View File

@@ -0,0 +1,140 @@
<div align="center">
<a href="https://github.com/kangfenmao/cherry-studio/releases">
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
</a>
</div>
<div align="center">
<a href="./README.md">English</a> | <a href="./README.zh.md">中文</a> | 日本語
</div>
# 🍒 Cherry Studio
![](https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505)
Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。
👏 [Telegramグループ](https://t.me/CherryStudioAI)に参加しましょう
# 🌠 スクリーンショット
![](https://github.com/user-attachments/assets/28585d83-4bf0-4714-b561-8c7bf57cc600)
![](https://github.com/user-attachments/assets/8576863a-f632-4776-bc12-657eeced9da3)
![](https://github.com/user-attachments/assets/790790d7-b462-48dd-bde1-91c1697a4648)
# 🌟 主な機能
1. **多様な LLM サービス対応**
- ☁️ 主要な LLM クラウドサービス対応OpenAI、Gemini、Anthropic など
- 🔗 AI Web サービス統合Claude、Peplexity、Poe など
- 💻 Ollama によるローカルモデル実行対応
2. **AI アシスタントと対話**
- 📚 300+ の事前設定済み AI アシスタント
- 🤖 カスタム AI アシスタントの作成
- 💬 複数モデルでの同時対話機能
3. **文書とデータ処理**
- 📄 テキスト、画像、Office、PDF など多様な形式対応
- ☁️ WebDAV によるファイル管理とバックアップ
- 📊 Mermaid による図表作成
- 💻 コードハイライト機能
4. **実用的なツール統合**
- 🔍 グローバル検索機能
- 📝 トピック管理システム
- 🔤 AI による翻訳機能
- 🎯 ドラッグ&ドロップによる整理
- 🔌 ミニプログラム対応
5. **優れたユーザー体験**
- 🖥️ Windows、Mac、Linux のクロスプラットフォーム対応
- 📦 環境構築不要ですぐに使用可能
- 🎨 ライト/ダークテーマと透明ウィンドウ対応
- 📝 完全な Markdown レンダリング
- 🤲 簡単な共有機能
# 🖥️ 開発
## IDEの設定
[Cursor](https://www.cursor.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## プロジェクトの設定
### インストール
```bash
$ yarn
```
### 開発
```bash
$ yarn dev
```
### ビルド
```bash
# Windowsの場合
$ yarn build:win
# macOSの場合
$ yarn build:mac
# Linuxの場合
$ yarn build:linux
```
# 🤝 貢献
Cherry Studioへの貢献を歓迎します以下の方法で貢献できます
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します。
2. **バグの修正**:見つけたバグを修正します。
3. **問題の管理**GitHubの問題を管理するのを手伝います。
4. **製品デザイン**:デザインの議論に参加します。
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します。
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します。
7. **使用の促進**Cherry Studioを広めます。
## 始め方
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします。
2. **ブランチを作成**:変更のためのブランチを作成します。
3. **変更を提出**:変更をコミットしてプッシュします。
4. **プルリクエストを開く**:変更内容と理由を説明します。
詳細なガイドラインについては、[貢献ガイド](./CONTRIBUTING.md)をご覧ください。
ご支援と貢献に感謝します!
# 🚀 コントリビューター
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
</a>
# コミュニティ
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
# 📣 プロダクトハント
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
# スポンサー
[Buy Me a Coffee](sponsor.md)
# 📃 ライセンス
[LICENSE](../LICENSE)
# ⭐️ スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)

View File

@@ -1,96 +1,141 @@
<div align="center"> <div align="center">
<a href="https://github.com/kangfenmao/cherry-studio/releases"> <a href="https://github.com/kangfenmao/cherry-studio/releases">
<img src="https://github.com/user-attachments/assets/995910f3-177a-4d1e-97ea-04e3b009ba36" alt="banner"/> <img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
</a> </a>
English / <a href="https://github.com/kangfenmao/cherry-studio">中文</a> </div>
<div align="center">
中文 / <a href="https://github.com/kangfenmao/cherry-studio">English</a> / <a href="./README.ja.md">日本語</a>
</div> </div>
# 🍒 Cherry Studio # 🍒 Cherry Studio
Cherry Studio 是一款跨平台桌面客户端支持多个大语言模型LLM服务商兼容 Windows、Mac 和 Linux 系统,并拥丰富的个性化选项与领先的功能设计。 ![](https://github.com/user-attachments/assets/995910f3-177a-4d1e-97ea-04e3b009ba36)
Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客户端兼容 Windows、Mac 和 Linux 系统。
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)
# 🌠 界面 # 🌠 界面
<img width="1582" alt="Xnip2024-09-23_15-01-53" src="https://github.com/user-attachments/assets/554aa31b-87b6-49fe-877d-af313e1608b0"> ![](https://github.com/user-attachments/assets/28585d83-4bf0-4714-b561-8c7bf57cc600)
<img width="1582" alt="Xnip2024-09-23_15-02-27" src="https://github.com/user-attachments/assets/f43fb4c8-194a-4f46-8575-6db2bd136cb9"> ![](https://github.com/user-attachments/assets/8576863a-f632-4776-bc12-657eeced9da3)
<img width="1582" alt="Xnip2024-09-23_16-12-19" src="https://github.com/user-attachments/assets/82ce3cc1-5a0b-49aa-9fe4-0376d34be1f8"> ![](https://github.com/user-attachments/assets/790790d7-b462-48dd-bde1-91c1697a4648)
<img width="1582" alt="Xnip2024-09-23_16-11-44" src="https://github.com/user-attachments/assets/55e420c8-fc0f-40a0-868e-d75bebeb5af3">
<img width="1582" alt="Xnip2024-09-23_16-11-50" src="https://github.com/user-attachments/assets/7413384e-a7c7-4525-96ea-ccd395d7e51a">
<img width="1582" alt="Xnip2024-09-23_16-12-59" src="https://github.com/user-attachments/assets/894b5e97-569f-4471-813c-c48d19455215">
# 🌟 特性 # 🌟 主要特性
## 😌 轻松上手 1. **多样化 LLM 服务支持**
🍏WindowsMacLinux跨平台支持 - ☁️ 支持主流 LLM 云服务OpenAI、Gemini、Anthropic、硅基流动等
- 🔗 集成流行 AI Web 服务Claude、Peplexity、Poe、腾讯元宝、知乎直答等
- 💻 支持 Ollama 本地模型部署
📦开箱即用,无需 Python 与 Docker 2. **智能助手与对话**
🤝简洁、友好的界面与交互设计 - 📚 内置 300+ 预配置 AI 助手
- 🤖 支持自定义创建专属助手
- 💬 多模型同时对话,获得多样化观点
## 🛠️多样化的 LLM 服务模式支持 3. **文档与数据处理**
☁️ 全面覆盖 LLM 云服务,支持自定义 api key 与模型管理OpenAIGeminiAnthropic硅基流动... - 📄 支持文本、图片、Office、PDF 等多种格式
- ☁️ WebDAV 文件管理与数据备份
- 📊 Mermaid 图表可视化
- 💻 代码高亮显示
🔗汇聚流行的 AI Web 服务并计划通过功能增强提升体验ClaudePeplexityPoe腾讯元宝知乎直答... 4. **实用工具集成**
💻支持 Ollama 运行本地模型 - 🔍 全局搜索功能
- 📝 话题管理系统
- 🔤 AI 驱动的翻译功能
- 🎯 拖拽排序
- 🔌 小程序支持
## 📲个性化的功能体验 5. **优质使用体验**
- 🖥️ Windows、Mac、Linux 跨平台支持
- 📦 开箱即用,无需配置环境
- 🎨 支持明暗主题与透明窗口
- 📝 完整的 Markdown 渲染
- 🤲 便捷的内容分享功能
📄完整的 Markdown 与 Mermaid 渲染支持 # 🖥️ 开发
🤖使用与创建智能体提升工作效率 ## IDE 设置
🔤持续迭代的翻译功能 [Cursor](https://www.cursor.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
🤲生成结果支持 Markdown 与图片分享
📎文件与图片上传RAG 与多模态对话
🎨透明窗口与明暗主题支持
# 🖥️ 开发指南
## 推荐的开发环境
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## 项目设置 ## 项目设置
### 安装依赖 ### 安装
```bash ```bash
$ yarn $ yarn
``` ```
### 启动开发环境 ### 开发
```bash ```bash
$ yarn dev $ yarn dev
``` ```
### 构建版本 ### 构建
```bash ```bash
# For windows # Windows
$ yarn build:win $ yarn build:win
# For macOS # macOS
$ yarn build:mac $ yarn build:mac
# For Linux # Linux
$ yarn build:linux $ yarn build:linux
``` ```
# 🤝 贡献
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
1. **贡献代码**:开发新功能或优化现有代码。
2. **修复错误**:提交您发现的错误修复。
3. **维护问题**:帮助管理 GitHub 问题。
4. **产品设计**:参与设计讨论。
5. **撰写文档**:改进用户手册和指南。
6. **社区参与**:加入讨论并帮助用户。
7. **推广使用**:宣传 Cherry Studio。
## 入门
1. **Fork 仓库**Fork 并克隆到您的本地机器。
2. **创建分支**:为您的更改创建分支。
3. **提交更改**:提交并推送您的更改。
4. **打开 Pull Request**:描述您的更改和原因。
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.md)。
感谢您的支持和贡献!
# 🚀 贡献者
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
</a>
<br /><br />
# 🌐 社区
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
# 📣 产品猎人
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
# ☕ 赞助
[微信赞赏码](sponsor.md)
# 📃 许可证
[LICENSE](../LICENSE)
# ⭐️ Star 记录 # ⭐️ Star 记录
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline) [![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
# 赞助
[微信赞赏码](docs/sponsor.md)
# 📃 许可证
[LICENSE](./LICENSE)

View File

@@ -1,95 +0,0 @@
# FAQ 文档
本文档适用于:产品手册、官网页面、课程测验、现场 Q&A。
## 问题1Cherry Studio 支持哪些操作系统?
- **答案**Cherry Studio 支持 Windows、Mac 和 Linux 操作系统。
## 问题2Cherry Studio 的主要功能有哪些?
- **答案**Cherry Studio 的主要功能包括:
1. 支持多个 LLM 提供商
2. 允许创建多个助手
3. 支持创建多个主题
4. 允许在同一对话中使用多个模型来回答问题
5. 支持拖放排序
6. 代码高亮
7. Mermaid 图表支持
## 问题3Cherry Studio 的主要目录结构是怎样的?
- **答案**Cherry Studio 的主要目录结构如下:
- `/src`: 主要源代码目录
- `/build`: 构建相关文件
- `/docs`: 文档目录
- `/resources`: 资源文件目录
- `/scripts`: 脚本文件目录
## 问题4如何在 Windows 环境下 fork Cherry Studio 并修改部分功能?
- **答案**:在 Windows 环境下 fork Cherry Studio 并修改部分功能的步骤如下:
1. 在 GitHub 上 fork Cherry Studio 仓库
2. 克隆 fork 的仓库到本地:`git clone https://github.com/your-username/cherry-studio.git`
3. 进入项目目录:`cd cherry-studio`
4. 安装依赖:`yarn install`
5. 修改所需的功能代码
6. 测试修改:`yarn dev`
7. 提交修改:`git add .``git commit -m "描述你的修改"`
8. 推送到你的 fork 仓库:`git push origin main`
## 问题5Cherry Studio 使用了哪些主要技术栈?
- **答案**Cherry Studio 主要使用了以下技术栈:
- TypeScript
- SCSS
- Electron
- Vite
- Sequelize
## 问题6如何贡献代码到 Cherry Studio 项目?
- **答案**:贡献代码到 Cherry Studio 项目的步骤如下:
1. Fork 项目仓库
2. 创建你的特性分支:`git checkout -b feature/AmazingFeature`
3. 提交你的修改:`git commit -m 'Add some AmazingFeature'`
4. 推送到分支:`git push origin feature/AmazingFeature`
5. 打开一个 Pull Request
## 问题7Cherry Studio 的 `/src` 目录主要包含哪些内容?
- **答案**Cherry Studio 的 `/src` 目录主要包含以下内容:
- 主进程代码Electron 主进程)
- 渲染进程代码(用户界面)
- 组件
- 工具函数
- 状态管理
- 样式文件
## 问题8如何在 Cherry Studio 中添加新的 LLM 提供商?
- **答案**:要在 Cherry Studio 中添加新的 LLM 提供商,你需要:
1.`/src/services` 或类似目录下创建新的服务文件
2. 实现与新 LLM 提供商 API 的集成
3. 在用户界面中添加新提供商的选项
4. 更新配置和状态管理以支持新提供商
## 问题9Cherry Studio 的构建过程是怎样的?
- **答案**Cherry Studio 的构建过程主要包括:
1. 使用 Vite 构建前端资源
2. 使用 Electron Builder 打包桌面应用
3. 根据不同平台Windows、Mac、Linux生成相应的安装包
## 问题10如何在 Cherry Studio 中实现新的 UI 主题?
- **答案**:在 Cherry Studio 中实现新的 UI 主题的步骤:
1.`/src/styles` 目录下创建新的主题 SCSS 文件
2. 定义新主题的颜色变量和样式
3. 在主样式文件中导入新主题
4. 更新主题切换逻辑以包含新主题
5. 在用户界面中添加新主题的选项
## 问题11Cherry Studio 如何处理多语言支持?
- **答案**Cherry Studio 可能通过以下方式处理多语言支持:
1. 使用 i18n 库进行国际化
2.`/src/locales` 或类似目录下存储不同语言的翻译文件
3. 实现语言切换功能
4. 在组件中使用翻译函数或组件来显示多语言文本
## 问题12如何为 Cherry Studio 编写单元测试?
- **答案**:为 Cherry Studio 编写单元测试的步骤:
1.`/tests` 目录下创建测试文件
2. 使用测试框架(如 Jest编写测试用例
3. 模拟 Electron 环境和其他依赖
4. 运行测试命令:`yarn test`
5. 确保测试覆盖主要功能和组件

View File

@@ -1,72 +0,0 @@
## Cherry Studio目录结构和功能
### 1. `/src`: 主要源代码目录
- ** `/main`**: Electron主进程相关代码
- 负责应用的生命周期管理、窗口创建、IPC通信等
- ** `/renderer`**: Electron渲染进程相关代码
- 包含用户界面的实现使用TypeScript和SCSS
- ** `/preload`**: 预加载脚本
- 用于在渲染进程中安全地暴露主进程功能
- ** `/components`**: React组件
- 可复用的UI组件如对话框、输入框等
- ** `/pages`**: 应用的主要页面
- 如聊天界面、设置页面等
- ** `/store`**: 状态管理
- 可能使用Redux或MobX来管理应用状态
- ** `/utils`**: 工具函数
- 包含各种辅助函数和工具类
- ** `/styles`**: 全局样式文件
- 包含SCSS文件定义全局样式和主题
### 2. `/public`: 静态资源目录
- 包含图标、字体等静态文件
### 3. `/electron`: Electron相关配置
- 包含Electron的构建和打包配置
### 4. `/scripts`: 构建和开发脚本
- 包含npm脚本用于开发、构建和部署
### 5. `/types`: TypeScript类型定义
- 包含自定义的类型定义文件
### 6. `/tests`: 测试文件目录
- 包含单元测试和集成测试
### 7. `/docs`: 文档目录
- 包含项目文档、API文档等
### 8. `/config`: 配置文件目录
- 包含各种配置文件如webpack配置、环境变量等
### 9. `/migrations`: 数据库迁移文件
- 由于使用了Sequelize这里可能包含数据库结构的变更记录
### 10. `/models`: 数据模型
- 定义Sequelize的数据模型对应数据库表结构
## 主要功能实现
### 1. LLM提供商集成
- 可能在`/src/utils``/src/services`中实现与不同LLM API的集成
### 2. 多助手和多主题支持
-`/src/store`中管理助手和主题的状态
-`/src/components`中实现相关的UI组件
### 3. 多模型对话
-`/src/pages`的聊天界面中实现
- 可能使用`/src/store`来管理对话状态
### 4. 拖放排序
-`/src/components`中实现相关的可拖拽组件
### 5. 代码高亮
- 可能使用第三方库如Prism.js集成在`/src/components`
### 6. Mermaid图表支持
-`/src/components`中集成Mermaid库
### 7. 数据持久化
- 使用Sequelize在`/models`中定义数据模型
-`/migrations`中管理数据库结构变更

View File

@@ -9,9 +9,8 @@ files:
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
- '!src' - '!src'
- '!local'
- '!scripts' - '!scripts'
- '!resources' - '!local'
asarUnpack: asarUnpack:
- resources/** - resources/**
win: win:
@@ -64,13 +63,11 @@ electronDownload:
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
本次更新: 输入内容支持快速翻译成英文
增加 Artifacts 网页预览功能 输出内容支持翻译成其他语言
内置助理新增网页生成助理 快速敲击3次空格翻译
DashScope 服务商修改为阿里云百炼 支持自定义快捷键
修复粘贴长文本后不能自动清除的问题 支持关闭对话自动重命名
话题右键菜单增加删除消息功能 修复 Gemini 自定义域名不生效问题
修复选择模型弹窗滚动条消失问题 画图支持生成 Seed 种子词
近期更新: 修复 Markdown 渲染错误导致应用崩溃
支持 PDF, DOC等办公文件格式
支持图片的预览和下载

View File

@@ -7,8 +7,9 @@ export default defineConfig({
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
resolve: { resolve: {
alias: { alias: {
'@main': resolve('src/main'),
'@types': resolve('src/renderer/src/types'), '@types': resolve('src/renderer/src/types'),
'@main': resolve('src/main') '@shared': resolve('packages/shared')
} }
} }
}, },
@@ -16,11 +17,15 @@ export default defineConfig({
plugins: [externalizeDepsPlugin()] plugins: [externalizeDepsPlugin()]
}, },
renderer: { renderer: {
plugins: [react()],
resolve: { resolve: {
alias: { alias: {
'@renderer': resolve('src/renderer/src') '@renderer': resolve('src/renderer/src'),
'@shared': resolve('packages/shared')
} }
}, },
plugins: [react()] optimizeDeps: {
exclude: []
}
} }
}) })

View File

@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "0.8.0", "version": "0.8.20",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@@ -8,7 +8,11 @@
"homepage": "https://github.com/kangfenmao/cherry-studio", "homepage": "https://github.com/kangfenmao/cherry-studio",
"workspaces": { "workspaces": {
"packages": [ "packages": [
"local" "local",
"packages/*"
],
"nohoist": [
"packages/database"
] ]
}, },
"scripts": { "scripts": {
@@ -27,20 +31,23 @@
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never", "build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
"release": "node scripts/version.js", "release": "node scripts/version.js",
"publish": "yarn release patch push", "publish": "yarn release patch push",
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
"generate:agents": "yarn workspace @cherry-studio/database agents",
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build" "generate:icons": "electron-icon-builder --input=./build/logo.png --output=build"
}, },
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"archiver": "^7.0.1", "adm-zip": "^0.5.16",
"docx": "^9.0.2",
"electron-log": "^5.1.5", "electron-log": "^5.1.5",
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
"electron-updater": "^6.1.7", "electron-updater": "^6.3.9",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"markdown-it": "^14.1.0",
"officeparser": "^4.1.1", "officeparser": "^4.1.1",
"unzipper": "^0.12.3",
"webdav": "4.11.4" "webdav": "4.11.4"
}, },
"devDependencies": { "devDependencies": {
@@ -52,13 +59,14 @@
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
"@types/adm-zip": "^0",
"@types/fs-extra": "^11", "@types/fs-extra": "^11",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@types/markdown-it": "^14",
"@types/node": "^18.19.9", "@types/node": "^18.19.9",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@types/tinycolor2": "^1", "@types/tinycolor2": "^1",
"@types/unzipper": "^0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"antd": "^5.18.3", "antd": "^5.18.3",
"axios": "^1.7.3", "axios": "^1.7.3",
@@ -84,24 +92,26 @@
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.4", "mime": "^4.0.4",
"openai": "^4.52.1", "openai": "patch:openai@npm%3A4.71.1#~/.yarn/patches/openai-npm-4.71.1-b5940d6401.patch",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.6.1",
"react-i18next": "^14.1.2", "react-i18next": "^14.1.2",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router": "6", "react-router": "6",
"react-router-dom": "6", "react-router-dom": "6",
"react-spinners": "^0.14.1", "react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"rehype-katex": "^7.0.1",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^6.0.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"sass": "^1.77.2", "sass": "^1.77.2",
"shiki": "^1.22.2",
"styled-components": "^6.1.11", "styled-components": "^6.1.11",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"typescript": "^5.6.2", "typescript": "^5.6.2",

View File

@@ -0,0 +1 @@
# Cherry Studio Artifacts

View File

@@ -0,0 +1,19 @@
{
"name": "@cherry-studio/artifacts",
"version": "0.1.0",
"description": "Cherry Studio Artifacts",
"main": "index.js",
"homepage": "https://github.com/kangfenmao/cherry-studio/blob/main/npm/artifacts",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"artifacts"
],
"author": "kangfenmao",
"license": "ISC"
}

View File

@@ -0,0 +1,108 @@
:root {
/* 莫兰迪色系:使用柔和、低饱和度的颜色 */
--primary-color: #b6b5a7; /* 莫兰迪灰褐色,用于背景文字 */
--secondary-color: #9a8f8f; /* 莫兰迪灰棕色,用于标题背景 */
--accent-color: #c5b4a0; /* 莫兰迪淡棕色,用于强调元素 */
--background-color: #e8e3de; /* 莫兰迪米色,用于页面背景 */
--text-color: #5b5b5b; /* 莫兰迪深灰色,用于主要文字 */
--light-text-color: #8c8c8c; /* 莫兰迪中灰色,用于次要文字 */
--divider-color: #d1cbc3; /* 莫兰迪浅灰色,用于分隔线 */
}
body,
html {
margin: 0;
padding: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--background-color); /* 使用莫兰迪米色作为页面背景 */
font-family: 'Noto Sans SC', sans-serif;
color: var(--text-color); /* 使用莫兰迪深灰色作为主要文字颜色 */
}
.card {
width: 300px;
height: 500px;
background-color: #f2ede9; /* 莫兰迪浅米色,用于卡片背景 */
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
.header {
background-color: var(--secondary-color); /* 使用莫兰迪灰棕色作为标题背景 */
color: #f2ede9; /* 浅色文字与深色背景形成对比 */
padding: 20px;
text-align: left;
position: relative;
z-index: 1;
}
h1 {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
margin: 0;
font-weight: 700;
}
.content {
padding: 30px 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.word {
text-align: left;
margin-bottom: 20px;
}
.word-main {
font-family: 'Noto Serif SC', serif;
font-size: 36px;
color: var(--text-color); /* 使用莫兰迪深灰色作为主要词汇颜色 */
margin-bottom: 10px;
position: relative;
}
.word-main::after {
content: '';
position: absolute;
left: 0;
bottom: -5px;
width: 50px;
height: 3px;
background-color: var(--accent-color); /* 使用莫兰迪淡棕色作为下划线 */
}
.word-sub {
font-size: 14px;
color: var(--light-text-color); /* 使用莫兰迪中灰色作为次要文字颜色 */
margin: 5px 0;
}
.divider {
width: 100%;
height: 1px;
background-color: var(--divider-color); /* 使用莫兰迪浅灰色作为分隔线 */
margin: 20px 0;
}
.explanation {
font-size: 18px;
line-height: 1.6;
text-align: left;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.quote {
position: relative;
padding-left: 20px;
border-left: 3px solid var(--accent-color); /* 使用莫兰迪淡棕色作为引用边框 */
}
.background-text {
position: absolute;
font-size: 150px;
color: rgba(182, 181, 167, 0.15); /* 使用莫兰迪灰褐色的透明版本作为背景文字 */
z-index: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}

3
packages/database/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
data/*
!data/.gitkeep

Binary file not shown.

View File

@@ -0,0 +1,3 @@
# Cherry Studio Database
Cherry Studio 依赖的数据文件由这个数据库来生成,数据库文件请联系开发者获取

View File

View File

@@ -0,0 +1,13 @@
{
"name": "@cherry-studio/database",
"packageManager": "yarn@4.3.1",
"dependencies": {
"csv-parser": "^3.0.0",
"sqlite3": "^5.1.7"
},
"scripts": {
"agents": "node src/agents.js",
"email": "yarn csv && node src/email.js",
"csv": "node src/csv.js"
}
}

View File

@@ -0,0 +1,47 @@
const sqlite3 = require('sqlite3').verbose()
const fs = require('fs')
// 连接到数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error connecting to the database:', err.message)
return
}
console.log('Connected to the database.')
})
// 查询数据并转换为JSON
db.all('SELECT * FROM agents', [], (err, rows) => {
if (err) {
console.error('Error querying the database:', err.message)
return
}
// 将 ID 类型转换为字符串
for (const row of rows) {
row.id = row.id.toString()
row.group = row.group.toString().split(',')
row.group = row.group.map((item) => item.trim().replace('\r\n', ''))
}
// 将查询结果转换为JSON字符串
const jsonData = JSON.stringify(rows, null, 2)
// 将JSON数据写入文件
fs.writeFile('../../src/renderer/src/config/agents.json', jsonData, (err) => {
if (err) {
console.error('Error writing to file:', err.message)
return
}
console.log('Data has been written to agents.json')
})
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing the database:', err.message)
return
}
console.log('Database connection closed.')
})
})

View File

@@ -0,0 +1,77 @@
const fs = require('fs')
const csv = require('csv-parser')
const sqlite3 = require('sqlite3').verbose()
// 连接到 SQLite 数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error opening database', err)
return
}
console.log('Connected to the SQLite database.')
})
// 创建一个数组来存储 CSV 数据
const results = []
// 读取 CSV 文件
fs.createReadStream('./data/data.csv')
.pipe(csv())
.on('data', (data) => results.push(data))
.on('end', () => {
// 准备 SQL 插入语句,使用 INSERT OR IGNORE
const stmt = db.prepare('INSERT OR IGNORE INTO emails (email, github, sent) VALUES (?, ?, ?)')
// 插入每一行数据
let inserted = 0
let skipped = 0
let emptyEmail = 0
db.serialize(() => {
// 开始一个事务以提高性能
db.run('BEGIN TRANSACTION')
results.forEach((row) => {
// 检查 email 是否为空
if (!row.email || row.email.trim() === '') {
emptyEmail++
return // 跳过这一行
}
stmt.run(row.email, row['user-href'], 0, function (err) {
if (err) {
console.error('Error inserting row', err)
} else {
if (this.changes === 1) {
inserted++
} else {
skipped++
}
}
})
})
// 提交事务
db.run('COMMIT', (err) => {
if (err) {
console.error('Error committing transaction', err)
} else {
console.log(
`Insertion complete. Inserted: ${inserted}, Skipped (duplicate): ${skipped}, Skipped (empty email): ${emptyEmail}`
)
}
// 完成插入
stmt.finalize()
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing database', err)
} else {
console.log('Database connection closed.')
}
})
})
})
})

View File

@@ -0,0 +1,36 @@
const sqlite3 = require('sqlite3').verbose()
// 连接到数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error connecting to the database:', err.message)
return
}
})
// 查询数据并转换为JSON
db.all('SELECT * FROM emails WHERE sent = 0', [], (err, rows) => {
if (err) {
console.error('Error querying the database:', err.message)
return
}
for (const row of rows) {
console.log(row.email)
// Update row set sent = 1
db.run('UPDATE emails SET sent = 1 WHERE id = ?', [row.id], (err) => {
if (err) {
console.error('Error updating the database:', err.message)
return
}
})
}
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing the database:', err.message)
return
}
})
})

1643
packages/database/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java' // Java 代码文件
]
export const ZOOM_SHORTCUTS = [
{
key: 'zoom_in',
shortcut: ['CommandOrControl', '='],
editable: false,
enabled: true
},
{
key: 'zoom_out',
shortcut: ['CommandOrControl', '-'],
editable: false,
enabled: true
},
{
key: 'zoom_reset',
shortcut: ['CommandOrControl', '0'],
editable: false,
enabled: true
}
]

View File

@@ -0,0 +1,118 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CherryStudio 许可协议-ZH/EN</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
</head>
<body class="bg-gray-100 p-8">
<div class="container mx-auto bg-white p-6 rounded shadow-lg">
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio 许可协议</h1>
<div class="mb-8">
<h2 class="text-2xl font-semibold mb-4">许可协议</h2>
<p class="mb-4">
本软件采用 <strong>Apache License 2.0</strong> 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry
Studio 时还应遵守以下附加条款:
</p>
<h3 class="text-xl font-semibold mb-2">一. 商用许可</h3>
<ol class="list-decimal list-inside mb-4">
<li><strong>免费商用</strong>:用户在不修改代码的情况下,可以免费用于商业目的。</li>
<li>
<strong>商业授权</strong>:如果您满足以下任意条件之一,需取得商业授权:
<ol class="list-decimal list-inside ml-4">
<li>对本软件进行二次修改、开发包括但不限于修改应用名称、logo、代码以及功能</li>
<li>为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。</li>
<li>预装或集成到硬件设备或产品中进行捆绑销售。</li>
<li>政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
</ol>
</li>
</ol>
<h3 class="text-xl font-semibold mb-2">二. 贡献者协议</h3>
<ol class="list-decimal list-inside mb-4">
<li><strong>许可调整</strong>:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。</li>
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
</ol>
<h3 class="text-xl font-semibold mb-2">三. 其他条款</h3>
<ol class="list-decimal list-inside mb-4">
<li>本协议条款的解释权归 Cherry Studio 开发者所有。</li>
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
</ol>
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p>
<p>
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问
<a href="http://www.apache.org/licenses/LICENSE-2.0"
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
</p>
</div>
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1>
<div class="mb-8">
<h2 class="text-2xl font-semibold mb-4">License Agreement</h2>
<p class="mb-4">
This software is licensed under the <strong>Apache License 2.0</strong>. In addition to the terms of the
Apache License 2.0, the following additional terms apply to the use of Cherry Studio:
</p>
<h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3>
<ol class="list-decimal list-inside mb-4">
<li>
<strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without
modifying
the code.
</li>
<li>
<strong>Commercial License Required</strong>: A commercial license is required if any of the
following
conditions are met:
<ol class="list-decimal list-inside ml-4">
<li>
You modify, develop, or alter the software, including but not limited to changes to the
application
name, logo, code, or functionality.
</li>
<li>You provide multi-tenant services to enterprise customers with 10 or more users.</li>
<li>
You pre-install or integrate the software into hardware devices or products and bundle it
for sale.
</li>
<li>
You are engaging in large-scale procurement for government or educational institutions,
especially
involving security, data privacy, or other sensitive requirements.
</li>
</ol>
</li>
</ol>
<h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3>
<ol class="list-decimal list-inside mb-4">
<li>
<strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source
license as
needed, making it stricter or more lenient.
</li>
<li>
<strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes,
including but
not limited to cloud business operations.
</li>
</ol>
<h3 class="text-xl font-semibold mb-2">III. Other Terms</h3>
<ol class="list-decimal list-inside mb-4">
<li>The interpretation of these terms is subject to the discretion of Cherry Studio developers.</li>
<li>These terms may be updated, and users will be notified through the software when changes occur.</li>
</ol>
<p class="mb-4">
For any questions or to request a commercial license, please contact the Cherry Studio development team.
</p>
<p>
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache
License 2.0. Detailed information about the Apache License 2.0 can be found at
<a href="http://www.apache.org/licenses/LICENSE-2.0"
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,595 @@
// 配置信息
const config = {
R2_CUSTOM_DOMAIN: 'cherrystudio.ocool.online',
R2_BUCKET_NAME: 'cherrystudio',
// 缓存键名
CACHE_KEY: 'cherry-studio-latest-release',
VERSION_DB: 'versions.json',
LOG_FILE: 'logs.json',
MAX_LOGS: 1000 // 最多保存多少条日志
};
// Worker 入口函数
const worker = {
// 定时器触发配置
scheduled: {
cron: '*/1 * * * *' // 每分钟执行一次
},
// 定时器执行函数 - 只负责检查和更新
async scheduled(event, env, ctx) {
try {
await initDataFiles(env);
console.log('开始定时检查新版本...');
// 注意这里使用新的函数
await checkNewRelease(env);
} catch (error) {
console.error('定时任务执行失败:', error);
}
},
// HTTP 请求处理函数 - 只负责返回数据
async fetch(request, env, ctx) {
if (!env || !env.R2_BUCKET) {
return new Response(JSON.stringify({
error: 'R2 存储桶未正确配置'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
const url = new URL(request.url);
const filename = url.pathname.slice(1);
try {
// 处理文件下载请求
if (filename) {
return await handleDownload(env, filename);
}
// 只返回缓存的版本信息
return await getCachedRelease(env);
} catch (error) {
return new Response(JSON.stringify({
error: error.message,
stack: error.stack
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
};
export default worker;
/**
* 添加日志记录函数
*/
async function addLog(env, type, event, details = null) {
try {
const logFile = await env.R2_BUCKET.get(config.LOG_FILE);
let logs = { logs: [] };
if (logFile) {
logs = JSON.parse(await logFile.text());
}
logs.logs.unshift({
timestamp: new Date().toISOString(),
type,
event,
details
});
// 保持日志数量在限制内
if (logs.logs.length > config.MAX_LOGS) {
logs.logs = logs.logs.slice(0, config.MAX_LOGS);
}
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2));
} catch (error) {
console.error('写入日志失败:', error);
}
}
/**
* 检查并更新发布版本
* 由定时器触发,检查新版本并更新 R2 存储
*/
async function checkAndUpdateRelease(env) {
try {
// 获取版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
let versions = { versions: {}, latestVersion: null, lastChecked: null };
if (versionDB) {
versions = JSON.parse(await versionDB.text());
}
// 获取 GitHub 最新版本
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
headers: { 'User-Agent': 'CloudflareWorker' },
});
if (!githubResponse.ok) {
throw new Error('GitHub API 请求失败');
}
const releaseData = await githubResponse.json();
const version = releaseData.tag_name;
// 更新最后检查时间
versions.lastChecked = new Date().toISOString();
// 检查是否需要更新
if (versions.latestVersion !== version) {
await addLog(env, 'INFO', `发现新版本: ${version}`);
// 准备新版本记录
const versionRecord = {
version,
publishedAt: releaseData.published_at,
uploadedAt: null,
files: releaseData.assets.map(asset => ({
name: asset.name,
size: asset.size,
uploaded: false
})),
changelog: releaseData.body
};
// 上传文件
for (const asset of releaseData.assets) {
try {
const existingFile = await env.R2_BUCKET.get(asset.name);
if (existingFile) {
// 更新文件状态
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
if (fileIndex !== -1) {
versionRecord.files[fileIndex].uploaded = true;
}
continue;
}
const response = await fetch(asset.browser_download_url);
if (!response.ok) {
throw new Error(`下载失败: HTTP ${response.status}`);
}
const file = await response.arrayBuffer();
await env.R2_BUCKET.put(asset.name, file, {
httpMetadata: { contentType: getContentType(asset.name) }
});
// 更新文件状态
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
if (fileIndex !== -1) {
versionRecord.files[fileIndex].uploaded = true;
}
await addLog(env, 'INFO', `文件上传成功: ${asset.name}`);
} catch (error) {
await addLog(env, 'ERROR', `文件上传失败: ${asset.name}`, error.message);
}
}
// 更新版本记录
versionRecord.uploadedAt = new Date().toISOString();
versions.versions[version] = versionRecord;
versions.latestVersion = version;
// 保存版本数据库
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
// 更新缓存
const cacheData = {
version,
publishedAt: releaseData.published_at,
changelog: releaseData.body,
downloads: versionRecord.files
.filter(file => file.uploaded)
.map(file => ({
name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size)
}))
};
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
// 清理旧版本
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a));
if (versionList.length > 2) {
const oldVersions = versionList.slice(2);
for (const oldVersion of oldVersions) {
const oldFiles = versions.versions[oldVersion].files;
for (const file of oldFiles) {
if (file.uploaded) {
await env.R2_BUCKET.delete(file.name);
await addLog(env, 'INFO', `删除旧文件: ${file.name}`);
}
}
delete versions.versions[oldVersion];
}
// 保存更新后的版本数据库
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
}
return cacheData;
} else {
// 没有新版本,返回缓存数据
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
return cached ? JSON.parse(await cached.text()) : null;
}
} catch (error) {
await addLog(env, 'ERROR', '检查更新失败', error.message);
throw error;
}
}
/**
* 获取最新版本信息
*/
async function getLatestRelease(env) {
try {
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
if (!cached) {
// 如果缓存不存在,先检查版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
if (versionDB) {
const versions = JSON.parse(await versionDB.text());
if (versions.latestVersion) {
// 从版本数据库重建缓存
const latestVersion = versions.versions[versions.latestVersion];
const cacheData = {
version: latestVersion.version,
publishedAt: latestVersion.publishedAt,
changelog: latestVersion.changelog,
downloads: latestVersion.files
.filter(file => file.uploaded)
.map(file => ({
name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size)
}))
};
// 更新缓存
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
return new Response(JSON.stringify(cacheData), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
}
// 如果版本数据库也没有数据,才执行检查更新
const data = await checkAndUpdateRelease(env);
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
const data = await cached.text();
return new Response(data, {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
} catch (error) {
await addLog(env, 'ERROR', '获取版本信息失败', error.message);
return new Response(JSON.stringify({
error: '获取版本信息失败: ' + error.message,
detail: '请稍后再试'
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
}
// 修改下载处理函数,直接接收 env
async function handleDownload(env, filename) {
try {
const object = await env.R2_BUCKET.get(filename);
if (!object) {
return new Response('文件未找到', { status: 404 });
}
// 设置响应头
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
headers.set('Content-Disposition', `attachment; filename="${filename}"`);
return new Response(object.body, {
headers
});
} catch (error) {
console.error('下载文件时发生错误:', error);
return new Response('获取文件失败', { status: 500 });
}
}
/**
* 根据文件扩展名获取对应的 Content-Type
*/
function getContentType(filename) {
const ext = filename.split('.').pop().toLowerCase();
const types = {
'exe': 'application/x-msdownload', // Windows 可执行文件
'dmg': 'application/x-apple-diskimage', // macOS 安装包
'zip': 'application/zip', // 压缩包
'AppImage': 'application/x-executable', // Linux 可执行文件
'blockmap': 'application/octet-stream' // 更新文件
};
return types[ext] || 'application/octet-stream';
}
/**
* 格式化文件大小
* 将字节转换为人类可读的格式B, KB, MB, GB
*/
function formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
/**
* 版本号比较函数
* 用于对版本号进行排序
*/
function compareVersions(a, b) {
const partsA = a.replace('v', '').split('.');
const partsB = b.replace('v', '').split('.');
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const numA = parseInt(partsA[i] || 0);
const numB = parseInt(partsB[i] || 0);
if (numA !== numB) {
return numA - numB;
}
}
return 0;
}
/**
* 初始化数据文件
*/
async function initDataFiles(env) {
try {
// 检查并初始化版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
if (!versionDB) {
const initialVersions = {
versions: {},
latestVersion: null,
lastChecked: new Date().toISOString()
};
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2));
await addLog(env, 'INFO', 'versions.json 初始化成功');
}
// 检查并初始化日志文件
const logFile = await env.R2_BUCKET.get(config.LOG_FILE);
if (!logFile) {
const initialLogs = {
logs: [{
timestamp: new Date().toISOString(),
type: 'INFO',
event: '系统初始化'
}]
};
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2));
console.log('logs.json 初始化成功');
}
} catch (error) {
console.error('初始化数据文件失败:', error);
}
}
// 新增:只获取缓存的版本信息
async function getCachedRelease(env) {
try {
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
if (!cached) {
// 如果缓存不存在,从版本数据库获取
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
if (versionDB) {
const versions = JSON.parse(await versionDB.text());
if (versions.latestVersion) {
const latestVersion = versions.versions[versions.latestVersion];
const cacheData = {
version: latestVersion.version,
publishedAt: latestVersion.publishedAt,
changelog: latestVersion.changelog,
downloads: latestVersion.files
.filter(file => file.uploaded)
.map(file => ({
name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size)
}))
};
// 重建缓存
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
return new Response(JSON.stringify(cacheData), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
}
// 如果没有任何数据,返回错误
return new Response(JSON.stringify({
error: '没有可用的版本信息'
}), {
status: 404,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// 返回缓存数据
return new Response(await cached.text(), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
} catch (error) {
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message);
throw error;
}
}
// 新增:只检查新版本并更新
async function checkNewRelease(env) {
try {
// 获取 GitHub 最新版本
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
headers: { 'User-Agent': 'CloudflareWorker' },
});
if (!githubResponse.ok) {
throw new Error('GitHub API 请求失败');
}
const releaseData = await githubResponse.json();
const version = releaseData.tag_name;
// 获取版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() };
if (versionDB) {
versions = JSON.parse(await versionDB.text());
}
// 如果版本相同,不需要更新
if (versions.latestVersion === version) {
console.log('当前已是最新版本');
return;
}
await addLog(env, 'INFO', `发现新版本: ${version}`);
// 准备新版本记录
const versionRecord = {
version,
publishedAt: releaseData.published_at,
uploadedAt: null,
files: releaseData.assets.map(asset => ({
name: asset.name,
size: asset.size,
uploaded: false
})),
changelog: releaseData.body
};
// 上传文件
for (const asset of releaseData.assets) {
try {
const existingFile = await env.R2_BUCKET.get(asset.name);
if (existingFile) {
// 更新文件状态
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
if (fileIndex !== -1) {
versionRecord.files[fileIndex].uploaded = true;
}
continue;
}
const response = await fetch(asset.browser_download_url);
if (!response.ok) {
throw new Error(`下载失败: HTTP ${response.status}`);
}
const file = await response.arrayBuffer();
await env.R2_BUCKET.put(asset.name, file, {
httpMetadata: { contentType: getContentType(asset.name) }
});
// 更新文件状态
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
if (fileIndex !== -1) {
versionRecord.files[fileIndex].uploaded = true;
}
await addLog(env, 'INFO', `文件上传成功: ${asset.name}`);
} catch (error) {
await addLog(env, 'ERROR', `文件上传失败: ${asset.name}`, error.message);
}
}
// 更新版本记录
versionRecord.uploadedAt = new Date().toISOString();
versions.versions[version] = versionRecord;
versions.latestVersion = version;
// 保存版本数据库
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
// 更新缓存
const cacheData = {
version,
publishedAt: releaseData.published_at,
changelog: releaseData.body,
downloads: versionRecord.files
.filter(file => file.uploaded)
.map(file => ({
name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size)
}))
};
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
// 清理旧版本
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a));
if (versionList.length > 2) {
const oldVersions = versionList.slice(2);
for (const oldVersion of oldVersions) {
const oldFiles = versions.versions[oldVersion].files;
for (const file of oldFiles) {
if (file.uploaded) {
await env.R2_BUCKET.delete(file.name);
await addLog(env, 'INFO', `删除旧文件: ${file.name}`);
}
}
delete versions.versions[oldVersion];
}
// 保存更新后的版本数据库
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
}
return cacheData;
} catch (error) {
await addLog(env, 'ERROR', '检查新版本失败', error.message);
throw error;
}
}

View File

@@ -1,25 +1,15 @@
import fs from 'node:fs'
import { app } from 'electron' import { app } from 'electron'
import Store from 'electron-store'
import path from 'path' import { getDataPath } from './utils'
const isDev = process.env.NODE_ENV === 'development' const isDev = process.env.NODE_ENV === 'development'
isDev && app.setPath('userData', app.getPath('userData') + 'Dev') if (isDev) {
app.setPath('userData', app.getPath('userData') + 'Dev')
const getDataPath = () => {
const dataPath = path.join(app.getPath('userData'), 'Data')
if (!fs.existsSync(dataPath)) {
fs.mkdirSync(dataPath, { recursive: true })
}
return dataPath
} }
export const DATA_PATH = getDataPath() export const DATA_PATH = getDataPath()
export const appConfig = new Store()
export const titleBarOverlayDark = { export const titleBarOverlayDark = {
height: 40, height: 40,
color: '#00000000', color: '#00000000',

View File

@@ -1,94 +1,3 @@
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'] export const isMac = process.platform === 'darwin'
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'] export const isWin = process.platform === 'win32'
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac'] export const isLinux = process.platform === 'linux'
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.properties', // 配置属性文件
'.latex', // LaTeX 文档文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.gradle', // Gradle 构建文件
'.kts' // Kotlin Script 文件
]

9
src/main/electron.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare global {
namespace Electron {
interface App {
isQuitting: boolean
}
}
}
export {}

View File

@@ -3,9 +3,16 @@ import { app, BrowserWindow } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer' import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import { registerIpc } from './ipc' import { registerIpc } from './ipc'
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { updateUserDataPath } from './utils/upgrade' import { updateUserDataPath } from './utils/upgrade'
import { createMainWindow } from './window'
// Check for single instance lock
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
} else {
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
@@ -15,20 +22,20 @@ app.whenReady().then(async () => {
// Set app user model id for windows // Set app user model id for windows
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio') electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
// Default open or close DevTools by F12 in development const mainWindow = windowService.createMainWindow()
// and ignore CommandOrControl + R in production. new TrayService()
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
app.on('activate', function () { app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the // On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createMainWindow() if (BrowserWindow.getAllWindows().length === 0) {
windowService.createMainWindow()
} else {
windowService.showMainWindow()
}
}) })
const mainWindow = createMainWindow() registerShortcuts(mainWindow)
registerIpc(mainWindow, app) registerIpc(mainWindow, app)
@@ -39,14 +46,24 @@ app.whenReady().then(async () => {
} }
}) })
// Quit when all windows are closed, except on macOS. There, it's common // Listen for second instance
// for applications and their menu bar to stay active until the user quits app.on('second-instance', () => {
// explicitly with Cmd + Q. const mainWindow = BrowserWindow.getAllWindows()[0]
app.on('window-all-closed', () => { if (mainWindow) {
if (process.platform !== 'darwin') { mainWindow.isMinimized() && mainWindow.restore()
app.quit() mainWindow.show()
mainWindow.focus()
} }
}) })
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
app.on('before-quit', () => {
app.isQuitting = true
})
// In this file you can include the rest of your app"s specific main process // In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here. // code. You can also put them in separate files and require them here.
}

View File

@@ -1,42 +1,104 @@
import { BrowserWindow, ipcMain, session, shell } from 'electron' import fs from 'node:fs'
import path from 'node:path'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config' import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
import log from 'electron-log'
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater' import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager' import BackupManager from './services/BackupManager'
import FileManager from './services/FileManager' import { configManager } from './services/ConfigManager'
import { ExportService } from './services/ExportService'
import FileStorage from './services/FileStorage'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { windowService } from './services/WindowService'
import { compress, decompress } from './utils/zip' import { compress, decompress } from './utils/zip'
import { createMinappWindow } from './window'
const fileManager = new FileManager() const fileManager = new FileStorage()
const backupManager = new BackupManager() const backupManager = new BackupManager()
const exportService = new ExportService(fileManager)
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow) const { autoUpdater } = new AppUpdater(mainWindow)
// IPC ipcMain.handle('app:info', () => ({
ipcMain.handle('get-app-info', () => ({
version: app.getVersion(), version: app.getVersion(),
isPackaged: app.isPackaged, isPackaged: app.isPackaged,
appPath: app.getAppPath() appPath: app.getAppPath(),
filesPath: path.join(app.getPath('userData'), 'Data', 'Files'),
appDataPath: app.getPath('userData'),
logsPath: log.transports.file.getFile().path
})) }))
ipcMain.handle('open-website', (_, url: string) => { ipcMain.handle('app:proxy', async (_, proxy: string) => {
shell.openExternal(url) const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
const proxyConfig: ProxyConfig = proxy === 'system' ? { mode: 'system' } : proxy ? { proxyRules: proxy } : {}
await Promise.all(sessions.map((session) => session.setProxy(proxyConfig)))
}) })
ipcMain.handle('set-proxy', (_, proxy: string) => { ipcMain.handle('app:reload', () => mainWindow.reload())
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {}) ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
// language
ipcMain.handle('app:set-language', (_, language) => {
configManager.setLanguage(language)
}) })
ipcMain.handle('reload', () => mainWindow.reload()) // tray
ipcMain.handle('app:set-tray', (_, isActive: boolean) => {
configManager.setTray(isActive)
})
// theme
ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => {
configManager.setTheme(theme)
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
})
// clear cache
ipcMain.handle('app:clear-cache', async () => {
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
try {
await Promise.all(
sessions.map(async (session) => {
await session.clearCache()
await session.clearStorageData({
storages: ['cookies', 'filesystem', 'shadercache', 'websql', 'serviceworkers', 'cachestorage']
})
})
)
await fileManager.clearTemp()
await fs.writeFileSync(log.transports.file.getFile().path, '')
return { success: true }
} catch (error: any) {
log.error('Failed to clear cache:', error)
return { success: false, error: error.message }
}
})
// check for update
ipcMain.handle('app:check-for-update', async () => {
const update = await autoUpdater.checkForUpdates()
return {
currentVersion: autoUpdater.currentVersion,
updateInfo: update?.updateInfo
}
})
// zip
ipcMain.handle('zip:compress', (_, text: string) => compress(text)) ipcMain.handle('zip:compress', (_, text: string) => compress(text))
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text)) ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
// backup
ipcMain.handle('backup:backup', backupManager.backup) ipcMain.handle('backup:backup', backupManager.backup)
ipcMain.handle('backup:restore', backupManager.restore) ipcMain.handle('backup:restore', backupManager.restore)
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav) ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav) ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
// file
ipcMain.handle('file:open', fileManager.open) ipcMain.handle('file:open', fileManager.open)
ipcMain.handle('file:save', fileManager.save) ipcMain.handle('file:save', fileManager.save)
ipcMain.handle('file:select', fileManager.selectFile) ipcMain.handle('file:select', fileManager.selectFile)
@@ -50,9 +112,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('file:write', fileManager.writeFile) ipcMain.handle('file:write', fileManager.writeFile)
ipcMain.handle('file:saveImage', fileManager.saveImage) ipcMain.handle('file:saveImage', fileManager.saveImage)
ipcMain.handle('file:base64Image', fileManager.base64Image) ipcMain.handle('file:base64Image', fileManager.base64Image)
ipcMain.handle('file:download', fileManager.downloadFile)
ipcMain.handle('file:copy', fileManager.copyFile)
// minapp
ipcMain.handle('minapp', (_, args) => { ipcMain.handle('minapp', (_, args) => {
createMinappWindow({ windowService.createMinappWindow({
url: args.url, url: args.url,
parent: mainWindow, parent: mainWindow,
windowOptions: { windowOptions: {
@@ -62,17 +127,21 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}) })
}) })
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => { // export
appConfig.set('theme', theme) ipcMain.handle('export:word', exportService.exportToWord)
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight) // open path
ipcMain.handle('open:path', async (_, path: string) => {
await shell.openPath(path)
}) })
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法) // shortcuts
ipcMain.handle('check-for-update', async () => { ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
return { configManager.setShortcuts(shortcuts)
currentVersion: autoUpdater.currentVersion, // Refresh shortcuts registration
update: await autoUpdater.checkForUpdates() if (mainWindow) {
unregisterAllShortcuts()
registerShortcuts(mainWindow)
} }
}) })
} }

BIN
src/main/resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

View File

@@ -1,4 +1,4 @@
import { BrowserWindow, dialog } from 'electron' import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log' import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater' import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
@@ -9,7 +9,7 @@ export default class AppUpdater {
logger.transports.file.level = 'debug' logger.transports.file.level = 'debug'
autoUpdater.logger = logger autoUpdater.logger = logger
autoUpdater.forceDevUpdateConfig = true autoUpdater.forceDevUpdateConfig = true
autoUpdater.autoDownload = false autoUpdater.autoDownload = true
// 检测下载错误 // 检测下载错误
autoUpdater.on('error', (error) => { autoUpdater.on('error', (error) => {
@@ -18,38 +18,8 @@ export default class AppUpdater {
}) })
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => { autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
autoUpdater.logger?.info('检测到新版本,确认是否下载') autoUpdater.logger?.info('检测到新版本,开始自动下载')
mainWindow.webContents.send('update-available', releaseInfo) mainWindow.webContents.send('update-available', releaseInfo)
const releaseNotes = releaseInfo.releaseNotes
let releaseContent = ''
if (releaseNotes) {
if (typeof releaseNotes === 'string') {
releaseContent = <string>releaseNotes
} else if (releaseNotes instanceof Array) {
releaseNotes.forEach((releaseNote) => {
releaseContent += `${releaseNote}\n`
})
}
} else {
releaseContent = '暂无更新说明'
}
// 弹框确认是否下载更新releaseContent是更新日志
dialog
.showMessageBox({
type: 'info',
title: '应用有新的更新',
detail: releaseContent,
message: '发现新版本,是否现在更新?',
buttons: ['下次再说', '更新']
})
.then(({ response }) => {
if (response === 1) {
logger.info('用户选择更新,准备下载更新')
mainWindow.webContents.send('download-update')
autoUpdater.downloadUpdate()
}
})
}) })
// 检测到不需要更新时 // 检测到不需要更新时
@@ -65,14 +35,21 @@ export default class AppUpdater {
// 当需要更新的内容下载完成后 // 当需要更新的内容下载完成后
autoUpdater.on('update-downloaded', () => { autoUpdater.on('update-downloaded', () => {
logger.info('下载完成,准备更新') logger.info('下载完成,询问用户是否更新')
dialog dialog
.showMessageBox({ .showMessageBox({
type: 'info',
title: '安装更新', title: '安装更新',
message: '更新下载完毕,应用将重启并进行安装' message: '更新下载完成,是否立即安装',
buttons: ['稍后安装', '立即安装'],
defaultId: 1,
cancelId: 0
}) })
.then(() => { .then(({ response }) => {
if (response === 1) {
app.isQuitting = true
setImmediate(() => autoUpdater.quitAndInstall()) setImmediate(() => autoUpdater.quitAndInstall())
}
}) })
}) })

View File

@@ -1,10 +1,9 @@
import { WebDavConfig } from '@types' import { WebDavConfig } from '@types'
import archiver from 'archiver' import AdmZip from 'adm-zip'
import { app } from 'electron' import { app } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import * as fs from 'fs-extra' import * as fs from 'fs-extra'
import * as path from 'path' import * as path from 'path'
import * as unzipper from 'unzipper'
import WebDav from './WebDav' import WebDav from './WebDav'
@@ -26,7 +25,6 @@ class BackupManager {
destinationPath: string = this.backupDir destinationPath: string = this.backupDir
): Promise<string> { ): Promise<string> {
try { try {
// 创建临时目录
await fs.ensureDir(this.tempDir) await fs.ensureDir(this.tempDir)
// 将 data 写入临时文件 // 将 data 写入临时文件
@@ -38,21 +36,16 @@ class BackupManager {
const tempDataDir = path.join(this.tempDir, 'Data') const tempDataDir = path.join(this.tempDir, 'Data')
await fs.copy(sourcePath, tempDataDir) await fs.copy(sourcePath, tempDataDir)
// 创建 zip 文件 // 使用 adm-zip 创建压缩文件
const output = fs.createWriteStream(path.join(destinationPath, fileName)) const zip = new AdmZip()
const archive = archiver('zip', { zlib: { level: 9 } }) zip.addLocalFolder(this.tempDir)
const backupedFilePath = path.join(destinationPath, fileName)
archive.pipe(output) zip.writeZip(backupedFilePath)
archive.directory(this.tempDir, false)
await archive.finalize()
// 清理临时目录 // 清理临时目录
await fs.remove(this.tempDir) await fs.remove(this.tempDir)
Logger.log('Backup completed successfully') Logger.log('Backup completed successfully')
const backupedFilePath = path.join(destinationPath, fileName)
return backupedFilePath return backupedFilePath
} catch (error) { } catch (error) {
Logger.error('Backup failed:', error) Logger.error('Backup failed:', error)
@@ -61,31 +54,43 @@ class BackupManager {
} }
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> { async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
try {
// 创建临时目录 // 创建临时目录
await fs.ensureDir(this.tempDir) await fs.ensureDir(this.tempDir)
// 解压备份文件到临时目录 Logger.log('[backup] step 1: unzip backup file', this.tempDir)
await fs
.createReadStream(backupPath) // 使用 adm-zip 解压
.pipe(unzipper.Extract({ path: this.tempDir })) const zip = new AdmZip(backupPath)
.promise() zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
Logger.log('[backup] step 2: read data.json')
// 读取 data.json // 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json') const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8') const data = await fs.readFile(dataPath, 'utf-8')
Logger.log('[backup] step 3: restore Data directory')
// 恢复 Data 目录 // 恢复 Data 目录
const sourcePath = path.join(this.tempDir, 'Data') const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data') const destPath = path.join(app.getPath('userData'), 'Data')
await fs.remove(destPath) await fs.remove(destPath)
await fs.copy(sourcePath, destPath) await fs.copy(sourcePath, destPath)
Logger.log('[backup] step 4: clean up temp directory')
// 清理临时目录 // 清理临时目录
await fs.remove(this.tempDir) await fs.remove(this.tempDir)
Logger.log('Restore completed successfully') Logger.log('[backup] step 5: Restore completed successfully')
return data return data
} catch (error) {
Logger.error('[backup] Restore failed:', error)
await fs.remove(this.tempDir).catch(() => {})
throw error
}
} }
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) { async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
@@ -102,7 +107,13 @@ class BackupManager {
const webdavClient = new WebDav(webdavConfig) const webdavClient = new WebDav(webdavConfig)
const retrievedFile = await webdavClient.getFileContents(filename) const retrievedFile = await webdavClient.getFileContents(filename)
const backupedFilePath = path.join(this.backupDir, filename) const backupedFilePath = path.join(this.backupDir, filename)
if (!fs.existsSync(this.backupDir)) {
fs.mkdirSync(this.backupDir, { recursive: true })
}
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer) await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
return await this.restore(_, backupedFilePath) return await this.restore(_, backupedFilePath)
} }
} }

View File

@@ -0,0 +1,85 @@
import { ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron'
import Store from 'electron-store'
import { locales } from '../utils/locales'
export class ConfigManager {
private store: Store
private subscribers: Map<string, Array<(newValue: any) => void>> = new Map()
constructor() {
this.store = new Store()
}
getLanguage(): LanguageVarious {
const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : 'en-US'
return this.store.get('language', locale) as LanguageVarious
}
setLanguage(theme: LanguageVarious) {
this.store.set('language', theme)
}
getTheme(): ThemeMode {
return this.store.get('theme', ThemeMode.light) as ThemeMode
}
setTheme(theme: ThemeMode) {
this.store.set('theme', theme)
}
isTray(): boolean {
return !!this.store.get('tray', true)
}
setTray(value: boolean) {
this.store.set('tray', value)
this.notifySubscribers('tray', value)
}
getZoomFactor(): number {
return this.store.get('zoomFactor', 1) as number
}
setZoomFactor(factor: number) {
this.store.set('zoomFactor', factor)
this.notifySubscribers('zoomFactor', factor)
}
subscribe<T>(key: string, callback: (newValue: T) => void) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, [])
}
this.subscribers.get(key)!.push(callback)
}
unsubscribe<T>(key: string, callback: (newValue: T) => void) {
const subscribers = this.subscribers.get(key)
if (subscribers) {
this.subscribers.set(
key,
subscribers.filter((subscriber) => subscriber !== callback)
)
}
}
private notifySubscribers<T>(key: string, newValue: T) {
const subscribers = this.subscribers.get(key)
if (subscribers) {
subscribers.forEach((subscriber) => subscriber(newValue))
}
}
getShortcuts() {
return this.store.get('shortcuts', ZOOM_SHORTCUTS) as Shortcut[] | []
}
setShortcuts(shortcuts: Shortcut[]) {
this.store.set('shortcuts', shortcuts)
this.notifySubscribers('shortcuts', shortcuts)
}
}
export const configManager = new ConfigManager()

View File

@@ -0,0 +1,222 @@
/* eslint-disable no-case-declarations */
// ExportService
import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, ShadingType, TextRun } from 'docx'
import { dialog } from 'electron'
import Logger from 'electron-log'
import MarkdownIt from 'markdown-it'
import FileStorage from './FileStorage'
export class ExportService {
private fileManager: FileStorage
private md: MarkdownIt
constructor(fileManager: FileStorage) {
this.fileManager = fileManager
this.md = new MarkdownIt()
}
private convertMarkdownToDocxElements(markdown: string) {
const tokens = this.md.parse(markdown, {})
const elements: any[] = []
let listLevel = 0
const processInlineTokens = (tokens: any[]): TextRun[] => {
const runs: TextRun[] = []
for (const token of tokens) {
switch (token.type) {
case 'text':
runs.push(new TextRun(token.content))
break
case 'strong':
runs.push(new TextRun({ text: token.content, bold: true }))
break
case 'em':
runs.push(new TextRun({ text: token.content, italics: true }))
break
case 'code_inline':
runs.push(new TextRun({ text: token.content, font: 'Consolas', size: 20 }))
break
}
}
return runs
}
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
switch (token.type) {
case 'heading_open':
// 获取标题级别 (h1 -> h6)
const level = parseInt(token.tag.slice(1)) as 1 | 2 | 3 | 4 | 5 | 6
const headingText = tokens[i + 1].content
elements.push(
new Paragraph({
text: headingText,
heading: HeadingLevel[`HEADING_${level}`],
spacing: {
before: 240,
after: 120
}
})
)
i += 2 // 跳过内容标记和闭合标记
break
case 'paragraph_open':
const inlineTokens = tokens[i + 1].children || []
elements.push(
new Paragraph({
children: processInlineTokens(inlineTokens),
spacing: {
before: 120,
after: 120
}
})
)
i += 2
break
case 'bullet_list_open':
listLevel++
break
case 'bullet_list_close':
listLevel--
break
case 'list_item_open':
const itemInlineTokens = tokens[i + 2].children || []
elements.push(
new Paragraph({
children: [
new TextRun({ text: '•', bold: true }),
new TextRun({ text: '\t' }),
...processInlineTokens(itemInlineTokens)
],
indent: {
left: listLevel * 720
}
})
)
i += 3
break
case 'fence': // 代码块
const codeLines = token.content.split('\n')
elements.push(
new Paragraph({
children: codeLines.map(
(line) =>
new TextRun({
text: line + '\n',
font: 'Consolas',
size: 20,
break: 1
})
),
shading: {
type: ShadingType.SOLID,
color: 'F5F5F5'
},
spacing: {
before: 120,
after: 120
},
border: {
top: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
bottom: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
left: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
right: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' }
}
})
)
break
case 'hr':
elements.push(
new Paragraph({
children: [new TextRun({ text: '─'.repeat(50), color: '999999' })],
alignment: AlignmentType.CENTER
})
)
break
case 'blockquote_open':
const quoteText = tokens[i + 2].content
elements.push(
new Paragraph({
children: [
new TextRun({
text: quoteText,
italics: true
})
],
indent: {
left: 720
},
border: {
left: {
style: BorderStyle.SINGLE,
size: 3,
color: 'CCCCCC'
}
},
spacing: {
before: 120,
after: 120
}
})
)
i += 3
break
}
}
return elements
}
public exportToWord = async (_: Electron.IpcMainInvokeEvent, markdown: string, fileName: string): Promise<void> => {
try {
const elements = this.convertMarkdownToDocxElements(markdown)
const doc = new Document({
styles: {
paragraphStyles: [
{
id: 'Normal',
name: 'Normal',
run: {
size: 24,
font: 'Arial'
}
}
]
},
sections: [
{
properties: {},
children: elements
}
]
})
const buffer = await Packer.toBuffer(doc)
const filePath = dialog.showSaveDialogSync({
title: '保存文件',
filters: [{ name: 'Word Document', extensions: ['docx'] }],
defaultPath: fileName
})
if (filePath) {
await this.fileManager.writeFile(_, filePath, buffer)
Logger.info('[ExportService] Document exported successfully')
}
} catch (error) {
Logger.error('[ExportService] Export to Word failed:', error)
throw error
}
}
}

View File

@@ -1,5 +1,5 @@
import { documentExts } from '@main/constant'
import { getFileType } from '@main/utils/file' import { getFileType } from '@main/utils/file'
import { documentExts, imageExts } from '@shared/config/constant'
import { FileType } from '@types' import { FileType } from '@types'
import * as crypto from 'crypto' import * as crypto from 'crypto'
import { import {
@@ -19,7 +19,7 @@ import * as path from 'path'
import { chdir } from 'process' import { chdir } from 'process'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
class FileManager { class FileStorage {
private storageDir = path.join(app.getPath('userData'), 'Data', 'Files') private storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
private tempDir = path.join(app.getPath('temp'), 'CherryStudio') private tempDir = path.join(app.getPath('temp'), 'CherryStudio')
@@ -119,6 +119,31 @@ class FileManager {
return Promise.all(fileMetadataPromises) return Promise.all(fileMetadataPromises)
} }
private async compressImage(sourcePath: string, destPath: string): Promise<void> {
try {
const stats = fs.statSync(sourcePath)
const fileSizeInMB = stats.size / (1024 * 1024)
// 如果图片大于1MB才进行压缩
if (fileSizeInMB > 1) {
try {
await fs.promises.copyFile(sourcePath, destPath)
logger.info('[FileStorage] Image compressed successfully:', sourcePath)
} catch (jimpError) {
logger.error('[FileStorage] Image compression failed:', jimpError)
await fs.promises.copyFile(sourcePath, destPath)
}
} else {
// 小图片直接复制
await fs.promises.copyFile(sourcePath, destPath)
}
} catch (error) {
logger.error('[FileStorage] Image handling failed:', error)
// 错误情况下直接复制原文件
await fs.promises.copyFile(sourcePath, destPath)
}
}
public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileType): Promise<FileType> => { public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileType): Promise<FileType> => {
const duplicateFile = await this.findDuplicateFile(file.path) const duplicateFile = await this.findDuplicateFile(file.path)
@@ -128,10 +153,18 @@ class FileManager {
const uuid = uuidv4() const uuid = uuidv4()
const origin_name = path.basename(file.path) const origin_name = path.basename(file.path)
const ext = path.extname(origin_name) const ext = path.extname(origin_name).toLowerCase()
const destPath = path.join(this.storageDir, uuid + ext) const destPath = path.join(this.storageDir, uuid + ext)
logger.info('[FileStorage] Uploading file:', file.path)
// 根据文件类型选择处理方式
if (imageExts.includes(ext)) {
await this.compressImage(file.path, destPath)
} else {
await fs.promises.copyFile(file.path, destPath) await fs.promises.copyFile(file.path, destPath)
}
const stats = await fs.promises.stat(destPath) const stats = await fs.promises.stat(destPath)
const fileType = getFileType(ext) const fileType = getFileType(ext)
@@ -234,6 +267,11 @@ class FileManager {
await this.initStorageDir() await this.initStorageDir()
} }
public clearTemp = async (): Promise<void> => {
await fs.promises.rmdir(this.tempDir, { recursive: true })
await fs.promises.mkdir(this.tempDir, { recursive: true })
}
public open = async ( public open = async (
_: Electron.IpcMainInvokeEvent, _: Electron.IpcMainInvokeEvent,
options: OpenDialogOptions options: OpenDialogOptions
@@ -265,7 +303,7 @@ class FileManager {
fileName: string, fileName: string,
content: string, content: string,
options?: SaveDialogOptions options?: SaveDialogOptions
): Promise<void> => { ): Promise<string | null> => {
try { try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({ const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件', title: '保存文件',
@@ -276,8 +314,11 @@ class FileManager {
if (!result.canceled && result.filePath) { if (!result.canceled && result.filePath) {
await writeFileSync(result.filePath, content, { encoding: 'utf-8' }) await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
} }
return result.filePath
} catch (err) { } catch (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err) logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
return null
} }
} }
@@ -315,6 +356,105 @@ class FileManager {
return null return null
} }
} }
public downloadFile = async (_: Electron.IpcMainInvokeEvent, url: string): Promise<FileType> => {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
} }
export default FileManager // 尝试从Content-Disposition获取文件名
const contentDisposition = response.headers.get('Content-Disposition')
let filename = 'download'
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i)
if (filenameMatch) {
filename = filenameMatch[1]
}
}
// 如果URL中有文件名使用URL中的文件名
const urlFilename = url.split('/').pop()
if (urlFilename && urlFilename.includes('.')) {
filename = urlFilename
}
// 如果文件名没有后缀根据Content-Type添加后缀
if (!filename.includes('.')) {
const contentType = response.headers.get('Content-Type')
const ext = this.getExtensionFromMimeType(contentType)
filename += ext
}
const uuid = uuidv4()
const ext = path.extname(filename)
const destPath = path.join(this.storageDir, uuid + ext)
// 将响应内容写入文件
const buffer = Buffer.from(await response.arrayBuffer())
await fs.promises.writeFile(destPath, buffer)
const stats = await fs.promises.stat(destPath)
const fileType = getFileType(ext)
const fileMetadata: FileType = {
id: uuid,
origin_name: filename,
name: uuid + ext,
path: destPath,
created_at: stats.birthtime,
size: stats.size,
ext: ext,
type: fileType,
count: 1
}
return fileMetadata
} catch (error) {
logger.error('[FileStorage] Download file error:', error)
throw error
}
}
private getExtensionFromMimeType(mimeType: string | null): string {
if (!mimeType) return '.bin'
const mimeToExtension: { [key: string]: string } = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'application/pdf': '.pdf',
'text/plain': '.txt',
'application/msword': '.doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/zip': '.zip',
'application/x-zip-compressed': '.zip',
'application/octet-stream': '.bin'
}
return mimeToExtension[mimeType] || '.bin'
}
public copyFile = async (_: Electron.IpcMainInvokeEvent, id: string, destPath: string): Promise<void> => {
try {
const sourcePath = path.join(this.storageDir, id)
// 确保目标目录存在
const destDir = path.dirname(destPath)
if (!fs.existsSync(destDir)) {
await fs.promises.mkdir(destDir, { recursive: true })
}
// 复制文件
await fs.promises.copyFile(sourcePath, destPath)
logger.info('[FileStorage] File copied successfully:', { from: sourcePath, to: destPath })
} catch (error) {
logger.error('[FileStorage] Copy file failed:', error)
throw error
}
}
}
export default FileStorage

View File

@@ -0,0 +1,118 @@
import { Shortcut } from '@types'
import { BrowserWindow, globalShortcut } from 'electron'
import { configManager } from './ConfigManager'
let showAppAccelerator: string | null = null
function getShortcutHandler(shortcut: Shortcut) {
switch (shortcut.key) {
case 'zoom_in':
return (window: BrowserWindow) => handleZoom(0.1)(window)
case 'zoom_out':
return (window: BrowserWindow) => handleZoom(-0.1)(window)
case 'zoom_reset':
return (window: BrowserWindow) => {
window.webContents.setZoomFactor(1)
configManager.setZoomFactor(1)
}
case 'show_app':
return (window: BrowserWindow) => {
if (window.isVisible()) {
window.hide()
} else {
window.show()
window.focus()
}
}
default:
return null
}
}
function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+')
}
function handleZoom(delta: number) {
return (window: BrowserWindow) => {
const currentZoom = window.webContents.getZoomFactor()
const newZoom = currentZoom + delta
if (newZoom >= 0.1 && newZoom <= 5.0) {
window.webContents.setZoomFactor(newZoom)
configManager.setZoomFactor(newZoom)
}
}
}
export function registerShortcuts(window: BrowserWindow) {
window.webContents.setZoomFactor(configManager.getZoomFactor())
const register = () => {
if (window.isDestroyed()) return
const shortcuts = configManager.getShortcuts()
if (!shortcuts) return
shortcuts.forEach((shortcut) => {
if (shortcut.shortcut.length === 0) {
return
}
const handler = getShortcutHandler(shortcut)
if (!handler) {
return
}
const accelerator = formatShortcutKey(shortcut.shortcut)
if (shortcut.key === 'show_app') {
showAppAccelerator = accelerator
}
if (shortcut.key.includes('zoom')) {
switch (shortcut.key) {
case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => shortcut.enabled && handler(window))
globalShortcut.register('CommandOrControl+numadd', () => shortcut.enabled && handler(window))
return
case 'zoom_out':
globalShortcut.register('CommandOrControl+-', () => shortcut.enabled && handler(window))
globalShortcut.register('CommandOrControl+numsub', () => shortcut.enabled && handler(window))
return
case 'zoom_reset':
globalShortcut.register('CommandOrControl+0', () => shortcut.enabled && handler(window))
return
}
}
if (shortcut.enabled) {
globalShortcut.register(accelerator, () => handler(window))
}
})
}
const unregister = () => {
if (window.isDestroyed()) return
globalShortcut.unregisterAll()
if (showAppAccelerator) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
}
}
window.on('focus', () => register())
window.on('blur', () => unregister())
if (!window.isDestroyed() && window.isFocused()) {
register()
}
}
export function unregisterAllShortcuts() {
showAppAccelerator = null
globalShortcut.unregisterAll()
}

View File

@@ -0,0 +1,90 @@
import { isMac } from '@main/constant'
import { locales } from '@main/utils/locales'
import { app, Menu, nativeImage, nativeTheme, Tray } from 'electron'
import icon from '../../../build/tray_icon.png?asset'
import iconDark from '../../../build/tray_icon_dark.png?asset'
import iconLight from '../../../build/tray_icon_light.png?asset'
import { configManager } from './ConfigManager'
import { windowService } from './WindowService'
export class TrayService {
private tray: Tray | null = null
constructor() {
this.updateTray()
this.watchTrayChanges()
}
private createTray() {
const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon
const tray = new Tray(iconPath)
if (process.platform === 'win32') {
tray.setImage(iconPath)
} else if (process.platform === 'darwin') {
const image = nativeImage.createFromPath(iconPath)
const resizedImage = image.resize({ width: 16, height: 16 })
resizedImage.setTemplateImage(true)
tray.setImage(resizedImage)
} else if (process.platform === 'linux') {
const image = nativeImage.createFromPath(iconPath)
const resizedImage = image.resize({ width: 16, height: 16 })
tray.setImage(resizedImage)
}
this.tray = tray
const locale = locales[configManager.getLanguage()]
const { tray: trayLocale } = locale.translation
const contextMenu = Menu.buildFromTemplate([
{
label: trayLocale.show_window,
click: () => windowService.showMainWindow()
},
{ type: 'separator' },
{
label: trayLocale.quit,
click: () => this.quit()
}
])
if (process.platform === 'linux') {
this.tray.setContextMenu(contextMenu)
}
this.tray.setToolTip('Cherry Studio')
this.tray.on('right-click', () => {
this.tray?.popUpContextMenu(contextMenu)
})
this.tray.on('click', () => {
windowService.showMainWindow()
})
}
private updateTray() {
if (configManager.isTray()) {
this.createTray()
} else {
this.destroyTray()
}
}
private destroyTray() {
if (this.tray) {
this.tray.destroy()
this.tray = null
}
}
private watchTrayChanges() {
configManager.subscribe<boolean>('tray', () => this.updateTray())
}
private quit() {
app.quit()
}
}

View File

@@ -12,7 +12,9 @@ export default class WebDav {
this.instance = createClient(params.webdavHost, { this.instance = createClient(params.webdavHost, {
username: params.webdavUser, username: params.webdavUser,
password: params.webdavPass password: params.webdavPass,
maxBodyLength: Infinity,
maxContentLength: Infinity
}) })
this.putFileContents = this.putFileContents.bind(this) this.putFileContents = this.putFileContents.bind(this)

View File

@@ -0,0 +1,198 @@
import { is } from '@electron-toolkit/utils'
import { isLinux, isWin } from '@main/constant'
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { locales } from '../utils/locales'
import { configManager } from './ConfigManager'
export class WindowService {
private static instance: WindowService | null = null
private mainWindow: BrowserWindow | null = null
public static getInstance(): WindowService {
if (!WindowService.instance) {
WindowService.instance = new WindowService()
}
return WindowService.instance
}
public createMainWindow(): BrowserWindow {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
return this.mainWindow
}
const mainWindowState = windowStateKeeper({
defaultWidth: 1080,
defaultHeight: 670
})
const theme = configManager.getTheme()
const isMac = process.platform === 'darwin'
this.mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 1080,
minHeight: 600,
show: true,
autoHideMenuBar: true,
transparent: isMac,
vibrancy: 'under-window',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
webviewTag: true
}
})
this.setupMainWindow(this.mainWindow, mainWindowState)
return this.mainWindow
}
public createMinappWindow({
url,
parent,
windowOptions
}: {
url: string
parent?: BrowserWindow
windowOptions?: Electron.BrowserWindowConstructorOptions
}): BrowserWindow {
const width = windowOptions?.width || 1000
const height = windowOptions?.height || 680
const minappWindow = new BrowserWindow({
width,
height,
autoHideMenuBar: true,
title: 'Cherry Studio',
...windowOptions,
parent,
webPreferences: {
preload: join(__dirname, '../preload/minapp.js'),
sandbox: false,
contextIsolation: false
}
})
minappWindow.loadURL(url)
return minappWindow
}
private setupMainWindow(mainWindow: BrowserWindow, mainWindowState: any) {
mainWindowState.manage(mainWindow)
this.setupContextMenu(mainWindow)
this.setupWindowEvents(mainWindow)
this.setupWebContentsHandlers(mainWindow)
this.setupWindowLifecycleEvents(mainWindow)
this.loadMainWindowContent(mainWindow)
}
private setupContextMenu(mainWindow: BrowserWindow) {
mainWindow.webContents.on('context-menu', () => {
const locale = locales[configManager.getLanguage()]
const { common } = locale.translation
const menu = new Menu()
menu.append(new MenuItem({ label: common.copy, role: 'copy' }))
menu.append(new MenuItem({ label: common.paste, role: 'paste' }))
menu.append(new MenuItem({ label: common.cut, role: 'cut' }))
menu.popup()
})
}
private setupWindowEvents(mainWindow: BrowserWindow) {
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
}
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
mainWindow.webContents.on('will-navigate', (event, url) => {
event.preventDefault()
shell.openExternal(url)
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
this.setupWebRequestHeaders(mainWindow)
}
private setupWebRequestHeaders(mainWindow: BrowserWindow) {
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
if (details.responseHeaders?.['X-Frame-Options']) {
delete details.responseHeaders['X-Frame-Options']
}
if (details.responseHeaders?.['x-frame-options']) {
delete details.responseHeaders['x-frame-options']
}
if (details.responseHeaders?.['Content-Security-Policy']) {
delete details.responseHeaders['Content-Security-Policy']
}
if (details.responseHeaders?.['content-security-policy']) {
delete details.responseHeaders['content-security-policy']
}
callback({ cancel: false, responseHeaders: details.responseHeaders })
})
}
private loadMainWindowContent(mainWindow: BrowserWindow) {
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
public getMainWindow(): BrowserWindow | null {
return this.mainWindow
}
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
mainWindow.on('close', (event) => {
const notInTray = !configManager.isTray()
// Windows and Linux
if ((isWin || isLinux) && notInTray) {
return app.quit()
}
// Mac
if (!app.isQuitting) {
event.preventDefault()
mainWindow.hide()
}
})
}
public showMainWindow() {
if (this.mainWindow) {
if (this.mainWindow.isMinimized()) {
return this.mainWindow.restore()
}
this.mainWindow.show()
this.mainWindow.focus()
} else {
this.createMainWindow()
}
}
}
export const windowService = WindowService.getInstance()

View File

@@ -1,6 +1,5 @@
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@main/constant' import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
import { FileTypes } from '@types'
import { FileTypes } from '../../renderer/src/types'
export function getFileType(ext: string): FileTypes { export function getFileType(ext: string): FileTypes {
ext = ext.toLowerCase() ext = ext.toLowerCase()

View File

@@ -1,3 +1,4 @@
import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { app } from 'electron' import { app } from 'electron'
@@ -5,3 +6,11 @@ import { app } from 'electron'
export function getResourcePath() { export function getResourcePath() {
return path.join(app.getAppPath(), 'resources') return path.join(app.getAppPath(), 'resources')
} }
export function getDataPath() {
const dataPath = path.join(app.getPath('userData'), 'Data')
if (!fs.existsSync(dataPath)) {
fs.mkdirSync(dataPath, { recursive: true })
}
return dataPath
}

13
src/main/utils/locales.ts Normal file
View File

@@ -0,0 +1,13 @@
import EnUs from '../../renderer/src/i18n/locales/en-us.json'
import RuRu from '../../renderer/src/i18n/locales/ru-ru.json'
import ZhCn from '../../renderer/src/i18n/locales/zh-cn.json'
import ZhTw from '../../renderer/src/i18n/locales/zh-tw.json'
const locales = {
'en-US': EnUs,
'zh-CN': ZhCn,
'zh-TW': ZhTw,
'ru-RU': RuRu
}
export { locales }

View File

@@ -0,0 +1,16 @@
function isTilingWindowManager() {
if (process.platform === 'darwin') {
return false
}
if (process.platform !== 'linux') {
return true
}
const desktopEnv = process.env.XDG_CURRENT_DESKTOP?.toLowerCase()
const tilingSystems = ['hyprland', 'i3', 'sway', 'bspwm', 'dwm', 'awesome', 'qtile', 'herbstluftwm', 'xmonad']
return tilingSystems.some((system) => desktopEnv?.includes(system))
}
export { isTilingWindowManager }

View File

@@ -1,127 +0,0 @@
import { is } from '@electron-toolkit/utils'
import { BrowserWindow, Menu, MenuItem, shell } from 'electron'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import icon from '../../build/icon.png?asset'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
export function createMainWindow() {
// Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({
defaultWidth: 1080,
defaultHeight: 670
})
const theme = appConfig.get('theme') || 'light'
// Create the browser window.
const isMac = process.platform === 'darwin'
const mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 1080,
minHeight: 600,
show: true,
autoHideMenuBar: true,
transparent: isMac,
vibrancy: 'fullscreen-ui',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
webviewTag: true
// devTools: !app.isPackaged,
}
})
mainWindowState.manage(mainWindow)
mainWindow.webContents.on('context-menu', () => {
const menu = new Menu()
menu.append(new MenuItem({ label: '复制', role: 'copy' }))
menu.append(new MenuItem({ label: '粘贴', role: 'paste' }))
menu.append(new MenuItem({ label: '剪切', role: 'cut' }))
menu.popup()
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.on('will-navigate', (event, url) => {
event.preventDefault()
shell.openExternal(url)
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
if (details.responseHeaders?.['X-Frame-Options']) {
delete details.responseHeaders['X-Frame-Options']
}
if (details.responseHeaders?.['x-frame-options']) {
delete details.responseHeaders['x-frame-options']
}
if (details.responseHeaders?.['Content-Security-Policy']) {
delete details.responseHeaders['Content-Security-Policy']
}
if (details.responseHeaders?.['content-security-policy']) {
delete details.responseHeaders['content-security-policy']
}
callback({ cancel: false, responseHeaders: details.responseHeaders })
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
return mainWindow
}
export function createMinappWindow({
url,
parent,
windowOptions
}: {
url: string
parent?: BrowserWindow
windowOptions?: Electron.BrowserWindowConstructorOptions
}) {
const width = windowOptions?.width || 1000
const height = windowOptions?.height || 680
const minappWindow = new BrowserWindow({
width,
height,
autoHideMenuBar: true,
title: 'Cherry Studio',
...windowOptions,
parent,
webPreferences: {
preload: join(__dirname, '../preload/minapp.js'),
sandbox: false,
contextIsolation: false
}
})
minappWindow.loadURL(url)
return minappWindow
}

View File

@@ -1,6 +1,7 @@
import { ElectronAPI } from '@electron-toolkit/preload' import { ElectronAPI } from '@electron-toolkit/preload'
import { FileType } from '@renderer/types' import { FileType } from '@renderer/types'
import { WebDavConfig } from '@renderer/types' import { WebDavConfig } from '@renderer/types'
import { AppInfo, LanguageVarious } from '@renderer/types'
import type { OpenDialogOptions } from 'electron' import type { OpenDialogOptions } from 'electron'
import { Readable } from 'stream' import { Readable } from 'stream'
@@ -8,19 +9,20 @@ declare global {
interface Window { interface Window {
electron: ElectronAPI electron: ElectronAPI
api: { api: {
getAppInfo: () => Promise<{ getAppInfo: () => Promise<AppInfo>
version: string
isPackaged: boolean
appPath: string
}>
checkForUpdate: () => void checkForUpdate: () => void
openWebsite: (url: string) => void openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void setProxy: (proxy: string | undefined) => void
setLanguage: (theme: LanguageVarious) => void
setTray: (isActive: boolean) => void
setTheme: (theme: 'light' | 'dark') => void setTheme: (theme: 'light' | 'dark') => void
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
reload: () => void reload: () => void
clearCache: () => Promise<{ success: boolean; error?: string }>
zip: {
compress: (text: string) => Promise<Buffer> compress: (text: string) => Promise<Buffer>
decompress: (text: Buffer) => Promise<string> decompress: (text: Buffer) => Promise<string>
}
backup: { backup: {
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable> backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
restore: (backupPath: string) => Promise<string> restore: (backupPath: string) => Promise<string>
@@ -38,9 +40,22 @@ declare global {
create: (fileName: string) => Promise<string> create: (fileName: string) => Promise<string>
write: (filePath: string, data: Uint8Array | string) => Promise<void> write: (filePath: string, data: Uint8Array | string) => Promise<void>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null> open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void save: (
path: string,
content: string | NodeJS.ArrayBufferView,
options?: SaveDialogOptions
) => Promise<string | null>
saveImage: (name: string, data: string) => void saveImage: (name: string, data: string) => void
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }> base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
download: (url: string) => Promise<FileType | null>
copy: (fileId: string, destPath: string) => Promise<void>
}
export: {
toWord: (markdown: string, fileName: string) => Promise<void>
}
openPath: (path: string) => Promise<void>
shortcuts: {
update: (shortcuts: Shortcut[]) => Promise<void>
} }
} }
} }

View File

@@ -1,18 +1,23 @@
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import { WebDavConfig } from '@types' import { Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron' import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer // Custom APIs for renderer
const api = { const api = {
getAppInfo: () => ipcRenderer.invoke('get-app-info'), getAppInfo: () => ipcRenderer.invoke('app:info'),
checkForUpdate: () => ipcRenderer.invoke('check-for-update'), reload: () => ipcRenderer.invoke('app:reload'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url), setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy),
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy), checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme), setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme),
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
minApp: (url: string) => ipcRenderer.invoke('minapp', url), minApp: (url: string) => ipcRenderer.invoke('minapp', url),
reload: () => ipcRenderer.invoke('reload'), clearCache: () => ipcRenderer.invoke('app:clear-cache'),
zip: {
compress: (text: string) => ipcRenderer.invoke('zip:compress', text), compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text), decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
},
backup: { backup: {
backup: (fileName: string, data: string, destinationPath?: string) => backup: (fileName: string, data: string, destinationPath?: string) =>
ipcRenderer.invoke('backup:backup', fileName, data, destinationPath), ipcRenderer.invoke('backup:backup', fileName, data, destinationPath),
@@ -35,7 +40,16 @@ const api = {
ipcRenderer.invoke('file:save', path, content, options), ipcRenderer.invoke('file:save', path, content, options),
selectFolder: () => ipcRenderer.invoke('file:selectFolder'), selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data), saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId) base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
download: (url: string) => ipcRenderer.invoke('file:download', url),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath)
},
export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
},
openPath: (path: string) => ipcRenderer.invoke('open:path', path),
shortcuts: {
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts)
} }
} }

View File

@@ -5,7 +5,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" /> <meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" /> content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<style> <style>
html, html,
body { body {

View File

@@ -8,12 +8,13 @@ import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar' import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView' import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider' import AntdProvider from './context/AntdProvider'
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
import { ThemeProvider } from './context/ThemeProvider' import { ThemeProvider } from './context/ThemeProvider'
import AgentsPage from './pages/agents/AgentsPage' import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage' import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage' import FilesPage from './pages/files/FilesPage'
import HistoryPage from './pages/history/HistoryPage'
import HomePage from './pages/home/HomePage' import HomePage from './pages/home/HomePage'
import PaintingsPage from './pages/paintings/PaintingsPage'
import SettingsPage from './pages/settings/SettingsPage' import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage' import TranslatePage from './pages/translate/TranslatePage'
@@ -22,6 +23,7 @@ function App(): JSX.Element {
<Provider store={store}> <Provider store={store}>
<ThemeProvider> <ThemeProvider>
<AntdProvider> <AntdProvider>
<SyntaxHighlighterProvider>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
<TopViewContainer> <TopViewContainer>
<HashRouter> <HashRouter>
@@ -30,14 +32,15 @@ function App(): JSX.Element {
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/files" element={<FilesPage />} /> <Route path="/files" element={<FilesPage />} />
<Route path="/agents" element={<AgentsPage />} /> <Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings" element={<PaintingsPage />} />
<Route path="/translate" element={<TranslatePage />} /> <Route path="/translate" element={<TranslatePage />} />
<Route path="/apps" element={<AppsPage />} /> <Route path="/apps" element={<AppsPage />} />
<Route path="/messages/*" element={<HistoryPage />} />
<Route path="/settings/*" element={<SettingsPage />} /> <Route path="/settings/*" element={<SettingsPage />} />
</Routes> </Routes>
</HashRouter> </HashRouter>
</TopViewContainer> </TopViewContainer>
</PersistGate> </PersistGate>
</SyntaxHighlighterProvider>
</AntdProvider> </AntdProvider>
</ThemeProvider> </ThemeProvider>
</Provider> </Provider>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.06667 4.73333C2.06667 3.26057 3.26057 2.06667 4.73333 2.06667H9V1H4.73333C2.67147 1 1 2.67147 1 4.73333V9H2.06667V4.73333ZM2.06667 15.2667C2.06667 16.7394 3.26057 17.9333 4.73333 17.9333H9V19H4.73333C2.67147 19 1 17.3285 1 15.2667V11H2.06667V15.2667ZM15.2667 2.06667C16.7394 2.06667 17.9333 3.26057 17.9333 4.73333V9H19V4.73333C19 2.67147 17.3285 1 15.2667 1H11V2.06667H15.2667ZM17.9333 15.2667C17.9333 16.7394 16.7394 17.9333 15.2667 17.9333H11V19H15.2667C17.3285 19 19 17.3285 19 15.2667V11H17.9333V15.2667Z" fill="#030712"/>
</svg>

After

Width:  |  Height:  |  Size: 683 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.93333 3.73333C4.93333 2.26057 5.978 1.06667 7.26667 1.06667H9V0H7.26667C5.46254 0 4 1.67147 4 3.73333V8H4.93333V3.73333ZM4.93333 16.2667C4.93333 17.7394 5.978 18.9333 7.26667 18.9333H9V20H7.26667C5.46254 20 4 18.3285 4 16.2667V12H4.93333V16.2667ZM13.7333 1.06667C15.022 1.06667 16.0667 2.26057 16.0667 3.73333V8H17V3.73333C17 1.67147 15.5375 0 13.7333 0H12V1.06667H13.7333ZM16.0667 16.2667C16.0667 17.7394 15.022 18.9333 13.7333 18.9333H12V20H13.7333C15.5375 20 17 18.3285 17 16.2667V12H16.0667V16.2667Z" fill="#030712"/>
</svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.06667 7.26667C1.06667 5.978 2.26057 4.93333 3.73333 4.93333H8V4H3.73333C1.67147 4 0 5.46254 0 7.26667V9H1.06667V7.26667ZM1.06667 11.2667C1.06667 12.7394 2.26057 13.9333 3.73333 13.9333H8V15H3.73333C1.67147 15 0 13.3285 0 11.2667V10H1.06667V11.2667ZM16.2667 4.93333C17.7394 4.93333 18.9333 5.978 18.9333 7.26667V9H20V7.26667C20 5.46254 18.3285 4 16.2667 4H12V4.93333H16.2667ZM18.9333 11.2667C18.9333 12.7394 17.7394 13.9333 16.2667 13.9333H12V15H16.2667C18.3285 15 20 13.3285 20 11.2667V10H18.9333V11.2667Z" fill="#030712"/>
</svg>

After

Width:  |  Height:  |  Size: 679 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.06667 5.26667C1.06667 3.978 2.26057 2.93333 3.73333 2.93333H8V2H3.73333C1.67147 2 0 3.46254 0 5.26667V9H1.06667V5.26667ZM1.06667 14.7333C1.06667 16.022 2.26057 17.0667 3.73333 17.0667H8V18H3.73333C1.67147 18 0 16.5375 0 14.7333V11H1.06667V14.7333ZM16.2667 2.93333C17.7394 2.93333 18.9333 3.978 18.9333 5.26667V9H20V5.26667C20 3.46254 18.3285 2 16.2667 2H12V2.93333H16.2667ZM18.9333 14.7333C18.9333 16.022 17.7394 17.0667 16.2667 17.0667H12V18H16.2667C18.3285 18 20 16.5375 20 14.7333V11H18.9333V14.7333Z" fill="#030712"/>
</svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.93333 3.73333C2.93333 2.26057 3.978 1.06667 5.26667 1.06667H9V0H5.26667C3.46254 0 2 1.67147 2 3.73333V8H2.93333V3.73333ZM2.93333 16.2667C2.93333 17.7394 3.978 18.9333 5.26667 18.9333H9V20H5.26667C3.46254 20 2 18.3285 2 16.2667V12H2.93333V16.2667ZM14.7333 1.06667C16.022 1.06667 17.0667 2.26057 17.0667 3.73333V8H18V3.73333C18 1.67147 16.5375 0 14.7333 0H11V1.06667H14.7333ZM17.0667 16.2667C17.0667 17.7394 16.022 18.9333 14.7333 18.9333H11V20H14.7333C16.5375 20 18 18.3285 18 16.2667V12H17.0667V16.2667Z" fill="#030712"/>
</svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.93333 3.73333C5.93333 2.26057 6.978 1.06667 8.26667 1.06667H10V0H8.26667C6.46254 0 5 1.67147 5 3.73333V8H5.93333V3.73333ZM5.93333 16.2667C5.93333 17.7394 6.978 18.9333 8.26667 18.9333H10V20H8.26667C6.46254 20 5 18.3285 5 16.2667V12H5.93333V16.2667ZM12.7333 1.06667C14.022 1.06667 15.0667 2.26057 15.0667 3.73333V8H16V3.73333C16 1.67147 14.5375 0 12.7333 0H11V1.06667H12.7333ZM15.0667 16.2667C15.0667 17.7394 14.022 18.9333 12.7333 18.9333H11V20H12.7333C14.5375 20 16 18.3285 16 16.2667V12H15.0667V16.2667Z" fill="#030712"/>
</svg>

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,7 +1,11 @@
#inputbar .ant-input { #inputbar {
resize: none; resize: none;
} }
.ant-image-preview-switch-left {
-webkit-app-region: no-drag;
}
.ant-btn:not(:disabled):focus-visible { .ant-btn:not(:disabled):focus-visible {
outline: none; outline: none;
} }
@@ -38,35 +42,6 @@
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
.segmented-tab {
.ant-segmented-item {
overflow: hidden;
}
.ant-segmented-item-selected {
background-color: var(--color-background-mute);
}
.ant-segmented-item-label {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
font-size: 13px;
}
.iconfont {
font-size: 13px;
margin-left: -2px;
}
.anticon-setting {
font-size: 12px;
}
.icon-business-smart-assistant {
margin-right: -2px;
}
.ant-segmented-item-icon + * {
margin-left: 4px;
}
}
.message-attachments { .message-attachments {
.ant-upload-list-item:hover { .ant-upload-list-item:hover {
background-color: initial !important; background-color: initial !important;

View File

@@ -1,6 +1,6 @@
@import './markdown.scss'; @import './markdown.scss';
@import './scrollbar.scss';
@import './ant.scss'; @import './ant.scss';
@import './scrollbar.scss';
@import '../fonts/icon-fonts/iconfont.css'; @import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/ubuntu/ubuntu.css'; @import '../fonts/ubuntu/ubuntu.css';
@@ -9,9 +9,9 @@
--color-white-soft: rgba(255, 255, 255, 0.8); --color-white-soft: rgba(255, 255, 255, 0.8);
--color-white-mute: rgba(255, 255, 255, 0.94); --color-white-mute: rgba(255, 255, 255, 0.94);
--color-black: #181818; --color-black: #151515;
--color-black-soft: #202020; --color-black-soft: #222222;
--color-black-mute: #262626; --color-black-mute: #333333;
--color-gray-1: #515c67; --color-gray-1: #515c67;
--color-gray-2: #414853; --color-gray-2: #414853;
@@ -32,15 +32,16 @@
--color-text: var(--color-text-1); --color-text: var(--color-text-1);
--color-icon: #ffffff99; --color-icon: #ffffff99;
--color-icon-white: #ffffff; --color-icon-white: #ffffff;
--color-border: #ffffff24; --color-border: #ffffff22;
--color-border-soft: #ffffff20; --color-border-soft: #ffffff11;
--color-border-mute: #ffffff11;
--color-error: #f44336; --color-error: #f44336;
--color-link: #1677ff; --color-link: #1677ff;
--color-code-background: #323232; --color-code-background: #323232;
--color-scrollbar-thumb: rgba(255, 255, 255, 0.08);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15);
--color-hover: rgba(40, 40, 40, 1); --color-hover: rgba(40, 40, 40, 1);
--color-active: rgba(55, 55, 55, 1); --color-active: rgba(55, 55, 55, 1);
--color-frame-border: #333;
--color-group-background: var(--color-background-soft);
--navbar-background-mac: rgba(30, 30, 30, 0.6); --navbar-background-mac: rgba(30, 30, 30, 0.6);
--navbar-background: rgba(30, 30, 30); --navbar-background: rgba(30, 30, 30);
@@ -48,11 +49,16 @@
--navbar-height: 40px; --navbar-height: 40px;
--sidebar-width: 50px; --sidebar-width: 50px;
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 85px; --input-bar-height: 100px;
--assistants-width: 275px; --assistants-width: 275px;
--topic-list-width: 275px; --topic-list-width: 275px;
--settings-width: 250px; --settings-width: 250px;
--chat-background: #111111;
--chat-background-user: #28b561;
--chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black);
} }
body[theme-mode='light'] { body[theme-mode='light'] {
@@ -84,17 +90,23 @@ body[theme-mode='light'] {
--color-icon: #00000099; --color-icon: #00000099;
--color-icon-white: #000000; --color-icon-white: #000000;
--color-border: #00000028; --color-border: #00000028;
--color-border-soft: #00000028; --color-border-soft: #00000020;
--color-border-mute: #00000010;
--color-error: #f44336; --color-error: #f44336;
--color-link: #1677ff; --color-link: #1677ff;
--color-code-background: #e3e3e3; --color-code-background: #e3e3e3;
--color-scrollbar-thumb: rgba(0, 0, 0, 0.08);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.15);
--color-hover: var(--color-white-mute); --color-hover: var(--color-white-mute);
--color-active: var(--color-white-soft); --color-active: var(--color-white-soft);
--color-frame-border: #ddd;
--color-group-background: var(--color-white);
--navbar-background-mac: rgba(255, 255, 255, 0.6); --navbar-background-mac: rgba(255, 255, 255, 0.6);
--navbar-background: rgba(255, 255, 255); --navbar-background: rgba(255, 255, 255);
--chat-background: #f3f3f3;
--chat-background-user: #95ec69;
--chat-background-assistant: #ffffff;
--chat-text-user: var(--color-text);
} }
*, *,
@@ -140,6 +152,7 @@ html,
body, body,
#root { #root {
height: 100%; height: 100%;
width: 100%;
margin: 0; margin: 0;
} }
@@ -160,7 +173,6 @@ body[os='mac'] {
#content-container { #content-container {
border-top-left-radius: 10px; border-top-left-radius: 10px;
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
border-top-right-radius: 10px;
border-left: 0.5px solid var(--color-border); border-left: 0.5px solid var(--color-border);
box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.05); box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.05);
} }
@@ -193,7 +205,51 @@ body[os='windows'] {
} }
.text-nowrap { .text-nowrap {
white-space: nowrap; display: -webkit-box !important;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: normal;
word-wrap: break-word;
}
.bubble {
background-color: var(--chat-background);
#chat-main {
background-color: var(--chat-background);
}
#messages {
background-color: var(--chat-background);
}
#inputbar {
border-radius: 0;
margin: 0;
border: none;
border-top: 1px solid var(--color-border-mute);
background: var(--color-background);
}
.system-prompt {
background-color: var(--chat-background-assistant);
}
.message-content-container {
margin: 5px 0;
border-radius: 8px;
padding: 10px 15px 0 15px;
}
.message-user {
color: var(--chat-text-user);
.markdown,
.anticon,
.iconfont,
.message-tokens {
color: var(--chat-text-user) !important;
}
.message-action-button:hover {
background-color: var(--color-white-soft);
}
}
code {
color: var(--color-text);
}
} }

View File

@@ -95,12 +95,7 @@
} }
span { span {
word-break: break-all; white-space: pre;
}
code {
white-space: pre-wrap !important;
font-family: 'Courier New', Courier, monospace;
} }
p code, p code,
@@ -108,14 +103,22 @@
background: var(--color-background-mute); background: var(--color-background-mute);
padding: 3px 5px; padding: 3px 5px;
border-radius: 5px; border-radius: 5px;
word-break: keep-all;
white-space: pre;
}
code {
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
} }
pre { pre {
white-space: pre-wrap !important;
border-radius: 5px; border-radius: 5px;
overflow-x: auto; overflow-x: auto;
font-family: 'Fira Code', 'Courier New', Courier, monospace; font-family: 'Fira Code', 'Courier New', Courier, monospace;
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
&:has(> .mermaid) {
background-color: transparent;
}
&:not(pre pre) { &:not(pre pre) {
> code:not(pre pre > code) { > code:not(pre pre > code) {
padding: 15px; padding: 15px;

View File

@@ -1,7 +1,21 @@
:root {
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.2);
--color-scrollbar-thumb-right: rgba(255, 255, 255, 0.18);
--color-scrollbar-thumb-right-hover: rgba(255, 255, 255, 0.25);
}
body[theme-mode='light'] {
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.2);
--color-scrollbar-thumb-right: rgba(0, 0, 0, 0.18);
--color-scrollbar-thumb-right-hover: rgba(0, 0, 0, 0.25);
}
/* 全局初始化滚动条样式 */ /* 全局初始化滚动条样式 */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 4px; width: 5px;
height: 2px; height: 5px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@@ -9,9 +23,17 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
border-radius: 10px;
background: var(--color-scrollbar-thumb); background: var(--color-scrollbar-thumb);
border-radius: 4px;
&:hover { &:hover {
background: var(--color-scrollbar-thumb-hover); background: var(--color-scrollbar-thumb-hover);
} }
} }
pre::-webkit-scrollbar-thumb {
border-radius: 0;
background: rgba(0, 0, 0, 0.08);
&:hover {
background: rgba(0, 0, 0, 0.15);
}
}

View File

@@ -0,0 +1,160 @@
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'
import { Assistant, AssistantMessage, AssistantSettings } from '@renderer/types'
import { Button, Card, Col, Divider, Form as FormAntd, FormInstance, Row, Space, Switch } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { FC, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
assistant: Assistant
updateAssistant: (assistant: Assistant) => void
updateAssistantSettings: (settings: Partial<AssistantSettings>) => void
}
const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
const { t } = useTranslation()
const [form] = Form.useForm()
const formRef = useRef<FormInstance>(null)
const [messages, setMessagess] = useState<AssistantMessage[]>(assistant?.messages || [])
const [hideMessages, setHideMessages] = useState(assistant?.settings?.hideMessages || false)
const showSaveButton = (assistant?.messages || []).length !== messages.length
const onSave = () => {
// 检查是否有空对话组
for (let i = 0; i < messages.length; i += 2) {
const userContent = messages[i].content.trim()
const assistantContent = messages[i + 1]?.content.trim()
if (userContent === '' || assistantContent === '') {
window.modal.error({
centered: true,
content: t('agents.edit.message.empty.content')
})
return
}
}
// 过滤掉空消息并将消息分组
const filteredMessagess = messages.reduce((acc, conv, index) => {
if (index % 2 === 0) {
const userContent = conv.content.trim()
const assistantContent = messages[index + 1]?.content.trim()
if (userContent !== '' || assistantContent !== '') {
acc.push({ role: 'user', content: userContent }, { role: 'assistant', content: assistantContent })
}
}
return acc
}, [] as AssistantMessage[])
updateAssistant({
...assistant,
messages: filteredMessagess
})
window.message.success({ content: t('message.save.success.title'), key: 'save-messages' })
}
const addMessages = () => {
setMessagess([...messages, { role: 'user', content: '' }, { role: 'assistant', content: '' }])
}
const updateMessages = (index: number, role: 'user' | 'assistant', content: string) => {
const newMessagess = [...messages]
newMessagess[index] = { role, content }
setMessagess(newMessagess)
}
const deleteMessages = (index: number) => {
const newMessagess = [...messages]
newMessagess.splice(index, 2) // 删除用户和助手的对话
setMessagess(newMessagess)
}
return (
<Container>
<Form ref={formRef} layout="vertical" form={form} labelAlign="right" colon={false}>
<Form.Item label={t('agents.edit.settings.hide_preset_messages')}>
<Switch
checked={hideMessages}
onChange={(checked) => {
setHideMessages(checked)
updateAssistantSettings({ hideMessages: checked })
}}
/>
</Form.Item>
<Divider style={{ marginBottom: 15 }} />
<Form.Item label={t('agents.edit.message.group.title')}>
{messages.map(
(_, index) =>
index % 2 === 0 && (
<Card
size="small"
key={index}
style={{ marginBottom: 16 }}
title={`${t('agents.edit.message.group.title')} #${index / 2 + 1}`}
extra={<Button icon={<DeleteOutlined />} type="text" danger onClick={() => deleteMessages(index)} />}>
<Row gutter={16} align="middle" style={{ marginBottom: 16 }}>
<Col span={3}>
<label>{t('agents.edit.message.user.title')}</label>
</Col>
<Col span={21}>
<TextArea
value={messages[index].content}
onChange={(e) => updateMessages(index, 'user', e.target.value)}
placeholder={t('agents.edit.message.user.placeholder')}
rows={1}
/>
</Col>
</Row>
<Row gutter={16} align="top">
<Col span={3}>
<label>{t('agents.edit.message.assistant.title')}</label>
</Col>
<Col span={21}>
<TextArea
value={messages[index + 1]?.content || ''}
onChange={(e) => updateMessages(index + 1, 'assistant', e.target.value)}
placeholder={t('agents.edit.message.assistant.placeholder')}
rows={3}
/>
</Col>
</Row>
</Card>
)
)}
<Space>
<Button icon={<PlusOutlined />} onClick={addMessages}>
{t('agents.edit.message.add.title')}
</Button>
</Space>
</Form.Item>
<Divider style={{ marginBottom: 15 }} />
<Form.Item>
{showSaveButton && (
<Button type="primary" onClick={onSave}>
{t('common.save')}
</Button>
)}
</Form.Item>
</Form>
<div style={{ minHeight: 50 }} />
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding-top: 10px;
`
const Form = styled(FormAntd)`
.ant-form-item-no-colon {
font-weight: 500;
}
`
export default AssistantMessagesSettings

View File

@@ -1,81 +1,107 @@
import { QuestionCircleOutlined } from '@ant-design/icons' import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant' import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { SettingRow } from '@renderer/pages/settings'
import { SettingRow, SettingRowTitle } from '@renderer/pages/settings'
import { Assistant, AssistantSettings } from '@renderer/types' import { Assistant, AssistantSettings } from '@renderer/types'
import { Button, Col, Row, Slider, Switch, Tooltip } from 'antd' import { Button, Col, Divider, Row, Slider, Switch, Tooltip } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import ModelAvatar from '../Avatar/ModelAvatar'
import SelectModelPopup from '../Popups/SelectModelPopup'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
updateAssistant: (assistant: Assistant) => void
updateAssistantSettings: (settings: Partial<AssistantSettings>) => void
} }
const AssistantModelSettings: FC<Props> = (props) => { const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE) const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT) const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false) const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [autoResetModel, setAutoResetModel] = useState(assistant?.settings?.autoResetModel ?? false)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true) const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
const { t } = useTranslation() const { t } = useTranslation()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
updateAssistantSettings({
temperature: settings.temperature ?? temperature,
contextCount: settings.contextCount ?? contextCount,
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
maxTokens: settings.maxTokens ?? maxTokens,
streamOutput: settings.streamOutput ?? streamOutput
})
}
const onTemperatureChange = (value) => { const onTemperatureChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
onUpdateAssistantSettings({ temperature: value }) updateAssistantSettings({ temperature: value })
} }
} }
const onConextCountChange = (value) => { const onContextCountChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
onUpdateAssistantSettings({ contextCount: value }) updateAssistantSettings({ contextCount: value })
} }
} }
const onMaxTokensChange = (value) => { const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) { if (!isNaN(value as number)) {
onUpdateAssistantSettings({ maxTokens: value }) updateAssistantSettings({ maxTokens: value })
} }
} }
const onReset = () => { const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE) setTemperature(DEFAULT_TEMPERATURE)
setConextCount(DEFAULT_CONEXTCOUNT) setContextCount(DEFAULT_CONTEXTCOUNT)
updateAssistant({ setEnableMaxTokens(false)
...assistant, setMaxTokens(0)
settings: { setStreamOutput(true)
...assistant.settings, updateAssistantSettings({
temperature: DEFAULT_TEMPERATURE, temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT, contextCount: DEFAULT_CONTEXTCOUNT,
enableMaxTokens: false, enableMaxTokens: false,
maxTokens: DEFAULT_MAX_TOKENS, maxTokens: 0,
streamOutput: true streamOutput: true
}
}) })
} }
useEffect(() => { const onSelectModel = async () => {
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE) const selectedModel = await SelectModelPopup.show({ model: assistant?.model })
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT) if (selectedModel) {
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false) setDefaultModel(selectedModel)
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS) updateAssistant({
setStreamOutput(assistant?.settings?.streamOutput ?? true) ...assistant,
}, [assistant]) defaultModel: selectedModel
})
}
}
return ( return (
<Container> <Container>
<Row align="middle" style={{ marginBottom: 10 }}>
<Label style={{ marginBottom: 10 }}>{t('assistants.settings.default_model')}</Label>
<Col span={24}>
<HStack alignItems="center">
<Button
icon={defaultModel ? <ModelAvatar model={defaultModel} size={20} /> : <PlusOutlined />}
onClick={onSelectModel}>
{defaultModel ? defaultModel.name : t('agents.edit.model.select.title')}
</Button>
</HStack>
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
<Label>
{t('assistants.settings.auto_reset_model')}{' '}
<Tooltip title={t('assistants.settings.auto_reset_model.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
<Switch
value={autoResetModel}
onChange={(checked) => {
setAutoResetModel(checked)
updateAssistantSettings({ autoResetModel: checked })
}}
/>
</SettingRow>
<Divider style={{ margin: '10px 0' }} />
<Row align="middle"> <Row align="middle">
<Label>{t('chat.settings.temperature')}</Label> <Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}> <Tooltip title={t('chat.settings.temperature.tip')}>
@@ -95,18 +121,20 @@ const AssistantModelSettings: FC<Props> = (props) => {
</Col> </Col>
</Row> </Row>
<Row align="middle"> <Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label> <Label>
<Tooltip title={t('chat.settings.conext_count.tip')}> {t('chat.settings.context_count')}{' '}
<Tooltip title={t('chat.settings.context_count.tip')}>
<QuestionIcon /> <QuestionIcon />
</Tooltip> </Tooltip>
</Label>
</Row> </Row>
<Row align="middle" gutter={10}> <Row align="middle" gutter={10}>
<Col span={24}> <Col span={24}>
<Slider <Slider
min={0} min={0}
max={20} max={20}
onChange={setConextCount} onChange={setContextCount}
onChangeComplete={onConextCountChange} onChangeComplete={onContextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0} value={typeof contextCount === 'number' ? contextCount : 0}
step={1} step={1}
/> />
@@ -123,7 +151,7 @@ const AssistantModelSettings: FC<Props> = (props) => {
checked={enableMaxTokens} checked={enableMaxTokens}
onChange={(enabled) => { onChange={(enabled) => {
setEnableMaxTokens(enabled) setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled }) updateAssistantSettings({ enableMaxTokens: enabled })
}} }}
/> />
</Row> </Row>
@@ -141,18 +169,17 @@ const AssistantModelSettings: FC<Props> = (props) => {
</Col> </Col>
</Row> </Row>
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('model.stream_output')}</SettingRowTitleSmall> <Label>{t('model.stream_output')}</Label>
<Switch <Switch
checked={streamOutput} checked={streamOutput}
onChange={(checked) => { onChange={(checked) => {
setStreamOutput(checked) setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked }) updateAssistantSettings({ streamOutput: checked })
}} }}
/> />
</SettingRow> </SettingRow>
<HStack <Divider style={{ margin: '15px 0' }} />
justifyContent="flex-end" <HStack justifyContent="flex-end">
style={{ marginTop: 20, padding: '10px 0', borderTop: '0.5px solid var(--color-border)' }}>
<Button onClick={onReset} style={{ width: 80 }} danger type="primary"> <Button onClick={onReset} style={{ width: 80 }} danger type="primary">
{t('chat.settings.reset')} {t('chat.settings.reset')}
</Button> </Button>
@@ -170,8 +197,8 @@ const Container = styled.div`
` `
const Label = styled.p` const Label = styled.p`
margin: 0;
margin-right: 5px; margin-right: 5px;
font-weight: 500;
` `
const QuestionIcon = styled(QuestionCircleOutlined)` const QuestionIcon = styled(QuestionCircleOutlined)`
@@ -180,8 +207,4 @@ const QuestionIcon = styled(QuestionCircleOutlined)`
color: var(--color-text-3); color: var(--color-text-3);
` `
const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
`
export default AssistantModelSettings export default AssistantModelSettings

View File

@@ -1,15 +1,20 @@
import { useAssistant } from '@renderer/hooks/useAssistant' import { Assistant, AssistantSettings } from '@renderer/types'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import { Assistant } from '@renderer/types'
import { Button, Input } from 'antd' import { Button, Input } from 'antd'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { Box, HStack, VStack } from '../Layout' import { Box, HStack } from '../Layout'
const AssistantPromptSettings: React.FC<{ assistant: Assistant; onOk: () => void }> = (props) => { interface Props {
const { assistant, updateAssistant } = useAssistant(props.assistant.id) assistant: Assistant
updateAssistant: (assistant: Assistant) => void
updateAssistantSettings: (settings: AssistantSettings) => void
onOk: () => void
}
const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant, onOk }) => {
const [name, setName] = useState(assistant.name) const [name, setName] = useState(assistant.name)
const [prompt, setPrompt] = useState(assistant.prompt) const [prompt, setPrompt] = useState(assistant.prompt)
const { t } = useTranslation() const { t } = useTranslation()
@@ -17,11 +22,10 @@ const AssistantPromptSettings: React.FC<{ assistant: Assistant; onOk: () => void
const onUpdate = () => { const onUpdate = () => {
const _assistant = { ...assistant, name, prompt } const _assistant = { ...assistant, name, prompt }
updateAssistant(_assistant) updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
} }
return ( return (
<VStack flex={1}> <Container>
<Box mb={8} style={{ fontWeight: 'bold' }}> <Box mb={8} style={{ fontWeight: 'bold' }}>
{t('common.name')} {t('common.name')}
</Box> </Box>
@@ -43,12 +47,20 @@ const AssistantPromptSettings: React.FC<{ assistant: Assistant; onOk: () => void
style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 150px)' }} style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 150px)' }}
/> />
<HStack width="100%" justifyContent="flex-end" mt="10px"> <HStack width="100%" justifyContent="flex-end" mt="10px">
<Button type="primary" onClick={props.onOk}> <Button type="primary" onClick={onOk}>
{t('common.close')} {t('common.close')}
</Button> </Button>
</HStack> </HStack>
</VStack> </Container>
) )
} }
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
padding: 5px;
`
export default AssistantPromptSettings export default AssistantPromptSettings

View File

@@ -1,4 +1,5 @@
import { useTheme } from '@renderer/context/ThemeProvider' import { useAgent } from '@renderer/hooks/useAgents'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { Menu, Modal } from 'antd' import { Menu, Modal } from 'antd'
import { useState } from 'react' import { useState } from 'react'
@@ -7,6 +8,7 @@ import styled from 'styled-components'
import { HStack } from '../Layout' import { HStack } from '../Layout'
import { TopView } from '../TopView' import { TopView } from '../TopView'
import AssistantMessagesSettings from './AssistantMessagesSettings'
import AssistantModelSettings from './AssistantModelSettings' import AssistantModelSettings from './AssistantModelSettings'
import AssistantPromptSettings from './AssistantPromptSettings' import AssistantPromptSettings from './AssistantPromptSettings'
@@ -18,32 +20,43 @@ interface Props extends AssistantSettingPopupShowParams {
resolve: (assistant: Assistant) => void resolve: (assistant: Assistant) => void
} }
const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve }) => { const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, ...props }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const { t } = useTranslation() const { t } = useTranslation()
const [menu, setMenu] = useState('prompt') const [menu, setMenu] = useState('prompt')
const { theme } = useTheme()
const _useAssistant = useAssistant(props.assistant.id)
const _useAgent = useAgent(props.assistant.id)
const isAgent = props.assistant.type === 'agent'
const assistant = isAgent ? _useAgent.agent : _useAssistant.assistant
const updateAssistant = isAgent ? _useAgent.updateAgent : _useAssistant.updateAssistant
const updateAssistantSettings = isAgent ? _useAgent.updateAgentSettings : _useAssistant.updateAssistantSettings
const onOk = () => { const onOk = () => {
setOpen(false) setOpen(false)
} }
const handleCancel = () => { const onCancel = () => {
setOpen(false) setOpen(false)
} }
const onClose = () => { const afterClose = () => {
resolve(assistant) resolve(assistant)
} }
const items = [ const items = [
{ {
key: 'prompt', key: 'prompt',
label: t('assistants.prompt_settings') label: t('assistants.settings.prompt')
}, },
{ {
key: 'model', key: 'model',
label: t('assistants.model_settings') label: t('assistants.settings.model')
},
{
key: 'messages',
label: t('assistants.settings.preset_messages')
} }
] ]
@@ -51,21 +64,20 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
<StyledModal <StyledModal
open={open} open={open}
onOk={onOk} onOk={onOk}
onCancel={handleCancel} onClose={onCancel}
afterClose={onClose} onCancel={onCancel}
transitionName="ant-move-down" afterClose={afterClose}
maskTransitionName="ant-fade"
footer={null} footer={null}
title={assistant.name} title={assistant.name}
transitionName="ant-move-down"
styles={{ styles={{
content: { content: {
padding: 0, padding: 0,
overflow: 'hidden', overflow: 'hidden',
border: '1px solid var(--color-border)', background: 'var(--color-background)',
background: 'var(--color-background)' border: `1px solid var(--color-frame-border)`
}, },
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0 }, header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0 }
mask: { background: theme === 'light' ? 'rgba(255,255,255, 0.8)' : 'rgba(0,0,0, 0.8)' }
}} }}
width="70vw" width="70vw"
height="80vh" height="80vh"
@@ -81,8 +93,28 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
/> />
</LeftMenu> </LeftMenu>
<Settings> <Settings>
{menu === 'prompt' && <AssistantPromptSettings assistant={assistant} onOk={onOk} />} {menu === 'prompt' && (
{menu === 'model' && <AssistantModelSettings assistant={assistant} />} <AssistantPromptSettings
assistant={assistant}
updateAssistant={updateAssistant}
updateAssistantSettings={updateAssistantSettings}
onOk={onOk}
/>
)}
{menu === 'model' && (
<AssistantModelSettings
assistant={assistant}
updateAssistant={updateAssistant}
updateAssistantSettings={updateAssistantSettings}
/>
)}
{menu === 'messages' && (
<AssistantMessagesSettings
assistant={assistant}
updateAssistant={updateAssistant}
updateAssistantSettings={updateAssistantSettings}
/>
)}
</Settings> </Settings>
</HStack> </HStack>
</StyledModal> </StyledModal>
@@ -111,10 +143,11 @@ const StyledModal = styled(Modal)`
} }
.ant-menu-item { .ant-menu-item {
height: 36px; height: 36px;
border-radius: 4px;
color: var(--color-text-2); color: var(--color-text-2);
display: flex; display: flex;
align-items: center; align-items: center;
border: 0.5px solid transparent;
border-radius: 6px;
.ant-menu-title-content { .ant-menu-title-content {
line-height: 36px; line-height: 36px;
} }
@@ -125,6 +158,7 @@ const StyledModal = styled(Modal)`
} }
.ant-menu-item-selected { .ant-menu-item-selected {
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
.ant-menu-title-content { .ant-menu-title-content {
color: var(--color-text-1); color: var(--color-text-1);
font-weight: 500; font-weight: 500;
@@ -132,11 +166,7 @@ const StyledModal = styled(Modal)`
} }
` `
export default class AssistantSettingPopup { export default class AssistantSettingsPopup {
static topviewId = 0
static hide() {
TopView.hide('AssistantSettingPopup')
}
static show(props: AssistantSettingPopupShowParams) { static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => { return new Promise<Assistant>((resolve) => {
TopView.show( TopView.show(
@@ -144,10 +174,10 @@ export default class AssistantSettingPopup {
{...props} {...props}
resolve={(v) => { resolve={(v) => {
resolve(v) resolve(v)
this.hide() TopView.hide('AssistantSettingsPopup')
}} }}
/>, />,
'AssistantSettingPopup' 'AssistantSettingsPopup'
) )
}) })
} }

View File

@@ -114,7 +114,15 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginTop: 15 }} /> <BeatLoader color="var(--color-text-2)" size="10" style={{ marginTop: 15 }} />
</EmptyView> </EmptyView>
)} )}
{opened && <webview src={app.url} ref={webviewRef} style={WebviewStyle} allowpopups={'true' as any} />} {opened && (
<webview
src={app.url}
ref={webviewRef}
style={WebviewStyle}
allowpopups={'true' as any}
partition="persist:webview"
/>
)}
</Drawer> </Drawer>
) )
} }

View File

@@ -3,15 +3,18 @@ import { TopView } from '@renderer/components/TopView'
import systemAgents from '@renderer/config/agents.json' import systemAgents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Agent, Assistant } from '@renderer/types' import { Agent, Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Divider, Input, InputRef, Modal, Tag } from 'antd' import { Divider, Input, InputRef, Modal, Tag } from 'antd'
import { take } from 'lodash'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { HStack } from '../Layout' import { HStack } from '../Layout'
import Scrollbar from '../Scrollbar'
interface Props { interface Props {
resolve: (value: Assistant | undefined) => void resolve: (value: Assistant | undefined) => void
@@ -26,35 +29,37 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const { assistants, addAssistant } = useAssistants() const { assistants, addAssistant } = useAssistants()
const inputRef = useRef<InputRef>(null) const inputRef = useRef<InputRef>(null)
const defaultAgent: Agent = useMemo(
() => ({
id: defaultAssistant.id,
name: defaultAssistant.name,
emoji: defaultAssistant.emoji || '',
prompt: defaultAssistant.prompt,
group: 'system'
}),
[defaultAssistant.emoji, defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
)
const agents = useMemo(() => { const agents = useMemo(() => {
const allAgents = [...userAgents, ...systemAgents] as Agent[] const allAgents = [...userAgents, ...systemAgents] as Agent[]
const list = [defaultAgent, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))] const list = [defaultAssistant, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
return searchText const filtered = searchText
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase())) ? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
: list : list
}, [assistants, defaultAgent, searchText, userAgents])
const onCreateAssistant = (agent: Agent) => { if (searchText.trim()) {
if (agent.id !== 'default') { const newAgent: Agent = {
if (assistants.map((a) => a.id).includes(String(agent.id))) { id: 'new',
return name: searchText.trim(),
prompt: '',
topics: [],
type: 'assistant',
emoji: '⭐️'
} }
return [newAgent, ...filtered]
} }
return filtered
}, [assistants, defaultAssistant, searchText, userAgents])
const assistant = covertAgentToAssistant(agent) const onCreateAssistant = async (agent: Agent) => {
let assistant: Assistant
if (agent.id === 'default') {
assistant = { ...agent, id: uuid() }
addAssistant(assistant) addAssistant(assistant)
} else {
assistant = await createAssistantFromAgent(agent)
}
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0) setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
resolve(assistant) resolve(assistant)
setOpen(false) setOpen(false)
@@ -79,9 +84,15 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
open={open} open={open}
onCancel={onCancel} onCancel={onCancel}
afterClose={onClose} afterClose={onClose}
transitionName="ant-move-down" transitionName="ant-move-up"
maskTransitionName="ant-fade" styles={{
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }} content: {
borderRadius: 20,
padding: 0,
overflow: 'hidden',
paddingBottom: 20
}
}}
closeIcon={null} closeIcon={null}
footer={null}> footer={null}>
<HStack style={{ padding: '0 12px', marginTop: 5 }}> <HStack style={{ padding: '0 12px', marginTop: 5 }}>
@@ -104,7 +115,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
</HStack> </HStack>
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} /> <Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
<Container> <Container>
{agents.map((agent) => ( {take(agents, 100).map((agent) => (
<AgentItem <AgentItem
key={agent.id} key={agent.id}
onClick={() => onCreateAssistant(agent)} onClick={() => onCreateAssistant(agent)}
@@ -112,8 +123,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
<HStack alignItems="center" gap={5}> <HStack alignItems="center" gap={5}>
{agent.emoji} {agent.name} {agent.emoji} {agent.name}
</HStack> </HStack>
{agent.group === 'system' && <Tag color="green">{t('agents.tag.system')}</Tag>} {agent.id === 'default' && <Tag color="green">{t('agents.tag.system')}</Tag>}
{agent.group === 'user' && <Tag color="orange">{t('agents.tag.user')}</Tag>} {agent.type === 'agent' && <Tag color="orange">{t('agents.tag.agent')}</Tag>}
{agent.id === 'new' && <Tag color="green">{t('agents.tag.new')}</Tag>}
</AgentItem> </AgentItem>
))} ))}
</Container> </Container>
@@ -121,14 +133,10 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
) )
} }
const Container = styled.div` const Container = styled(Scrollbar)`
padding: 0 12px; padding: 0 12px;
height: 50vh; height: 50vh;
margin-top: 10px; margin-top: 10px;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
` `
const AgentItem = styled.div` const AgentItem = styled.div`
@@ -161,7 +169,7 @@ const SearchIcon = styled.div`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: var(--color-background-soft); background-color: var(--color-background-mute);
margin-right: 2px; margin-right: 2px;
` `

View File

@@ -1,4 +1,5 @@
import { Input, InputProps, Modal } from 'antd' import { Input, Modal } from 'antd'
import { TextAreaProps } from 'antd/es/input'
import { useState } from 'react' import { useState } from 'react'
import { Box } from '../Layout' import { Box } from '../Layout'
@@ -9,7 +10,7 @@ interface PromptPopupShowParams {
message: string message: string
defaultValue?: string defaultValue?: string
inputPlaceholder?: string inputPlaceholder?: string
inputProps?: InputProps inputProps?: TextAreaProps
} }
interface Props extends PromptPopupShowParams { interface Props extends PromptPopupShowParams {
@@ -42,13 +43,14 @@ const PromptPopupContainer: React.FC<Props> = ({
return ( return (
<Modal title={title} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose} centered> <Modal title={title} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose} centered>
<Box mb={8}>{message}</Box> <Box mb={8}>{message}</Box>
<Input <Input.TextArea
placeholder={inputPlaceholder} placeholder={inputPlaceholder}
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
allowClear allowClear
autoFocus autoFocus
onPressEnter={onOk} onPressEnter={onOk}
rows={1}
{...inputProps} {...inputProps}
/> />
</Modal> </Modal>

View File

@@ -0,0 +1,69 @@
import HistoryPage from '@renderer/pages/history/HistoryPage'
import { Modal } from 'antd'
import { useState } from 'react'
import { TopView } from '../TopView'
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
SearchPopup.hide = onCancel
return (
<Modal
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
title={null}
width="920px"
transitionName="ant-move-down"
styles={{
content: {
padding: 0,
border: `1px solid var(--color-frame-border)`
},
body: { height: '85vh' }
}}
centered
footer={null}>
<HistoryPage />
</Modal>
)
}
export default class SearchPopup {
static topviewId = 0
static hide() {
TopView.hide('SearchPopup')
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide('SearchPopup')
}}
/>,
'SearchPopup'
)
})
}
}

View File

@@ -1,9 +1,10 @@
import { SearchOutlined } from '@ant-design/icons' import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon' import VisionIcon from '@renderer/components/Icons/VisionIcon'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { getModelLogo, isVisionModel } from '@renderer/config/models' import { getModelLogo, isVisionModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/model' import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd' import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
import { first, reverse, sortBy } from 'lodash' import { first, reverse, sortBy } from 'lodash'
@@ -12,6 +13,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { HStack } from '../Layout' import { HStack } from '../Layout'
import Scrollbar from '../Scrollbar'
type MenuItem = Required<MenuProps>['items'][number] type MenuItem = Required<MenuProps>['items'][number]
@@ -29,20 +31,58 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const inputRef = useRef<InputRef>(null) const inputRef = useRef<InputRef>(null)
const { providers } = useProviders() const { providers } = useProviders()
const [pinnedModels, setPinnedModels] = useState<string[]>([])
useEffect(() => {
const loadPinnedModels = async () => {
const setting = await db.settings.get('pinned:models')
const savedPinnedModels = setting?.value || []
// Filter out invalid pinned models
const allModelIds = providers.flatMap((p) => p.models || []).map((m) => getModelUniqId(m))
const validPinnedModels = savedPinnedModels.filter((id) => allModelIds.includes(id))
// Update storage if there were invalid models
if (validPinnedModels.length !== savedPinnedModels.length) {
await db.settings.put({ id: 'pinned:models', value: validPinnedModels })
}
setPinnedModels(validPinnedModels)
}
loadPinnedModels()
}, [providers])
const togglePin = async (modelId: string) => {
const newPinnedModels = pinnedModels.includes(modelId)
? pinnedModels.filter((id) => id !== modelId)
: [...pinnedModels, modelId]
await db.settings.put({ id: 'pinned:models', value: newPinnedModels })
setPinnedModels(newPinnedModels)
}
const filteredItems: MenuItem[] = providers const filteredItems: MenuItem[] = providers
.filter((p) => p.models && p.models.length > 0) .filter((p) => p.models && p.models.length > 0)
.map((p) => ({ .map((p) => {
key: p.id, const filteredModels = reverse(sortBy(p.models, 'name'))
label: p.isSystem ? t(`provider.${p.id}`) : p.name, .filter((m) =>
type: 'group', [m.name + m.provider + t('provider.' + p.id)].join('').toLowerCase().includes(searchText.toLowerCase())
children: reverse(sortBy(p.models, 'name')) )
.filter((m) => m.name.toLowerCase().includes(searchText.toLowerCase()))
.map((m) => ({ .map((m) => ({
key: getModelUniqId(m), key: getModelUniqId(m),
label: ( label: (
<ModelItem> <ModelItem>
<span>
{m?.name} {isVisionModel(m) && <VisionIcon />} {m?.name} {isVisionModel(m) && <VisionIcon />}
</span>
<PinIcon
onClick={(e) => {
e.stopPropagation()
togglePin(getModelUniqId(m))
}}
isPinned={pinnedModels.includes(getModelUniqId(m))}>
<PushpinOutlined />
</PinIcon>
</ModelItem> </ModelItem>
), ),
icon: ( icon: (
@@ -55,8 +95,58 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
setOpen(false) setOpen(false)
} }
})) }))
// Only return the group if it has filtered models
return filteredModels.length > 0
? {
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: filteredModels
}
: null
})
.filter(Boolean) as MenuItem[] // Filter out null items
if (pinnedModels.length > 0 && searchText.length === 0) {
const pinnedItems = providers
.flatMap((p) => p.models || [])
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
.map((m) => ({
key: getModelUniqId(m),
label: (
<ModelItem>
{m?.name} {isVisionModel(m) && <VisionIcon />}
<PinIcon
onClick={(e) => {
e.stopPropagation()
togglePin(getModelUniqId(m))
}}
isPinned={true}>
<PushpinOutlined />
</PinIcon>
</ModelItem>
),
icon: (
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
),
onClick: () => {
resolve(m)
setOpen(false)
}
})) }))
.filter((item) => item.children && item.children.length > 0) as MenuItem[]
if (pinnedItems.length > 0) {
filteredItems.unshift({
key: 'pinned',
label: t('model.pinned'),
type: 'group',
children: pinnedItems
} as MenuItem)
}
}
const onCancel = () => { const onCancel = () => {
setOpen(false) setOpen(false)
@@ -78,8 +168,15 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
onCancel={onCancel} onCancel={onCancel}
afterClose={onClose} afterClose={onClose}
transitionName="ant-move-down" transitionName="ant-move-down"
maskTransitionName="ant-fade" styles={{
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }} content: {
borderRadius: 20,
padding: 0,
overflow: 'hidden',
paddingBottom: 20,
border: '1px solid var(--color-border)'
}
}}
closeIcon={null} closeIcon={null}
footer={null}> footer={null}>
<HStack style={{ padding: '0 12px', marginTop: 5 }}> <HStack style={{ padding: '0 12px', marginTop: 5 }}>
@@ -101,6 +198,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
/> />
</HStack> </HStack>
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} /> <Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
<Scrollbar style={{ height: '50vh' }}>
<Container> <Container>
{filteredItems.length > 0 ? ( {filteredItems.length > 0 ? (
<StyledMenu <StyledMenu
@@ -115,14 +213,13 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
</EmptyState> </EmptyState>
)} )}
</Container> </Container>
</Scrollbar>
</Modal> </Modal>
) )
} }
const Container = styled.div` const Container = styled.div`
height: 50vh;
margin-top: 10px; margin-top: 10px;
overflow-y: auto;
` `
const StyledMenu = styled(Menu)` const StyledMenu = styled(Menu)`
@@ -139,6 +236,23 @@ const StyledMenu = styled(Menu)`
.ant-menu-item { .ant-menu-item {
height: 36px; height: 36px;
line-height: 36px; line-height: 36px;
&.ant-menu-item-selected {
background-color: var(--color-background-mute) !important;
color: var(--color-text-primary) !important;
}
&:not([data-menu-id^='pinned-']) {
.pin-icon {
opacity: 0;
}
&:hover {
.pin-icon {
opacity: 0.3;
}
}
}
} }
` `
@@ -146,6 +260,8 @@ const ModelItem = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
position: relative;
width: 100%;
` `
const EmptyState = styled.div` const EmptyState = styled.div`
@@ -167,8 +283,23 @@ const SearchIcon = styled.div`
margin-right: 2px; margin-right: 2px;
` `
const PinIcon = styled.span.attrs({ className: 'pin-icon' })<{ isPinned: boolean }>`
margin-left: auto;
padding: 0 8px;
opacity: ${(props) => (props.isPinned ? 1 : 'inherit')};
transition: opacity 0.2s;
position: absolute;
right: 0;
color: ${(props) => (props.isPinned ? 'var(--color-primary)' : 'inherit')};
transform: ${(props) => (props.isPinned ? 'rotate(-45deg)' : 'none')};
&:hover {
opacity: 1 !important;
color: ${(props) => (props.isPinned ? 'var(--color-primary)' : 'inherit')};
}
`
export default class SelectModelPopup { export default class SelectModelPopup {
static topviewId = 0
static hide() { static hide() {
TopView.hide('SelectModelPopup') TopView.hide('SelectModelPopup')
} }

View File

@@ -27,17 +27,28 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
resolve({}) resolve({})
} }
TemplatePopup.hide = onCancel
return ( return (
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose}> <Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
centered>
<Box mb={8}>Name</Box> <Box mb={8}>Name</Box>
</Modal> </Modal>
) )
} }
const TopViewKey = 'TemplatePopup'
export default class TemplatePopup { export default class TemplatePopup {
static topviewId = 0 static topviewId = 0
static hide() { static hide() {
TopView.hide('TemplatePopup') TopView.hide(TopViewKey)
} }
static show(props: ShowParams) { static show(props: ShowParams) {
return new Promise<any>((resolve) => { return new Promise<any>((resolve) => {
@@ -46,10 +57,10 @@ export default class TemplatePopup {
{...props} {...props}
resolve={(v) => { resolve={(v) => {
resolve(v) resolve(v)
this.hide() TopView.hide(TopViewKey)
}} }}
/>, />,
'TemplatePopup' TopViewKey
) )
}) })
} }

View File

@@ -55,7 +55,6 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
width="60vw" width="60vw"
style={{ maxHeight: '70vh' }} style={{ maxHeight: '70vh' }}
transitionName="ant-move-down" transitionName="ant-move-down"
maskTransitionName="ant-fade"
okText={t('common.save')} okText={t('common.save')}
{...modalProps} {...modalProps}
open={open} open={open}

View File

@@ -1,6 +1,6 @@
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import ImageStorage from '@renderer/services/storage' import ImageStorage from '@renderer/services/ImageStorage'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { setUserName } from '@renderer/store/settings' import { setUserName } from '@renderer/store/settings'

View File

@@ -0,0 +1,57 @@
import { throttle } from 'lodash'
import { FC, forwardRef, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
interface Props extends React.HTMLAttributes<HTMLDivElement> {
right?: boolean
ref?: any
}
const Scrollbar: FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
const [isScrolling, setIsScrolling] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const handleScroll = useCallback(
throttle(() => {
setIsScrolling(true)
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
timeoutRef.current = setTimeout(() => setIsScrolling(false), 1500) // 增加到 2 秒
}, 200),
[]
)
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
return (
<Container {...props} isScrolling={isScrolling} onScroll={handleScroll} ref={ref}>
{props.children}
</Container>
)
})
const Container = styled.div<{ isScrolling: boolean; right?: boolean }>`
overflow-y: auto;
&::-webkit-scrollbar-thumb {
transition: background 2s ease;
background: ${(props) =>
props.isScrolling ? `var(--color-scrollbar-thumb${props.right ? '-right' : ''})` : 'transparent'};
&:hover {
background: ${(props) =>
props.isScrolling ? `var(--color-scrollbar-thumb${props.right ? '-right' : ''}-hover)` : 'transparent'};
}
}
`
Scrollbar.displayName = 'Scrollbar'
export default Scrollbar

View File

@@ -0,0 +1,121 @@
import { LoadingOutlined, TranslationOutlined } from '@ant-design/icons'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { fetchTranslate } from '@renderer/services/ApiService'
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
import { getUserMessage } from '@renderer/services/MessagesService'
import { Button, Tooltip } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
text?: string
onTranslated: (translatedText: string) => void
disabled?: boolean
style?: React.CSSProperties
isLoading?: boolean
}
const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoading }) => {
const { t } = useTranslation()
const { translateModel } = useDefaultModel()
const [isTranslating, setIsTranslating] = useState(false)
const translateConfirm = () => {
return window?.modal?.confirm({
title: t('translate.confirm.title'),
content: t('translate.confirm.content'),
centered: true
})
}
const handleTranslate = async () => {
if (!text?.trim()) return
if (!(await translateConfirm())) {
return
}
if (!translateModel) {
window.message.error({
content: t('translate.error.not_configured'),
key: 'translate-message'
})
return
}
// 先复制原文到剪贴板
await navigator.clipboard.writeText(text)
setIsTranslating(true)
try {
const assistant = getDefaultTranslateAssistant('english', text)
const message = getUserMessage({
assistant,
topic: getDefaultTopic('default'),
type: 'text'
})
const translatedText = await fetchTranslate({ message, assistant })
onTranslated(translatedText)
} catch (error) {
console.error('Translation failed:', error)
window.message.error({
content: t('translate.error.failed'),
key: 'translate-message'
})
} finally {
setIsTranslating(false)
}
}
useEffect(() => {
setIsTranslating(isLoading ?? false)
}, [isLoading])
return (
<Tooltip placement="top" title={t('chat.input.translate')} arrow>
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
{isTranslating ? <LoadingOutlined spin /> : <TranslationOutlined />}
</ToolbarButton>
</Tooltip>
)
}
const ToolbarButton = styled(Button)`
min-width: 30px;
height: 30px;
font-size: 17px;
border-radius: 50%;
transition: all 0.3s ease;
color: var(--color-icon);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0;
&.anticon,
&.iconfont {
transition: all 0.3s ease;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-soft);
.anticon,
.iconfont {
color: var(--color-text-1);
}
}
&.active {
background-color: var(--color-primary) !important;
.anticon,
.iconfont {
color: var(--color-white-soft);
}
&:hover {
background-color: var(--color-primary);
}
}
`
export default TranslateButton

View File

@@ -38,7 +38,6 @@ const NavbarContainer = styled.div`
max-height: var(--navbar-height); max-height: var(--navbar-height);
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0}; margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
padding-left: ${isMac ? 'var(--sidebar-width)' : 0}; padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
transition: background-color 0.3s ease;
-webkit-app-region: drag; -webkit-app-region: drag;
` `

View File

@@ -1,9 +1,11 @@
import { FileSearchOutlined, FolderOutlined, TranslationOutlined } from '@ant-design/icons' import { FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env' import { isLocalAi, UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useRuntime } from '@renderer/hooks/useStore' import { Tooltip } from 'antd'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -21,6 +23,7 @@ const Sidebar: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { windowStyle } = useSettings() const { windowStyle } = useSettings()
const { theme, toggleTheme } = useTheme()
const isRoute = (path: string): string => (pathname === path ? 'active' : '') const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '') const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
@@ -48,44 +51,67 @@ const Sidebar: FC = () => {
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus> <MainMenus>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/')}> <StyledLink onClick={() => to('/')}>
<Icon className={isRoute('/')}> <Icon className={isRoute('/')}>
<i className="iconfont icon-chat" /> <i className="iconfont icon-chat" />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip>
<Tooltip title={t('agents.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/agents')}> <StyledLink onClick={() => to('/agents')}>
<Icon className={isRoute('/agents')}> <Icon className={isRoutes('/agents')}>
<i className="iconfont icon-business-smart-assistant" /> <i className="iconfont icon-business-smart-assistant" />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip>
<Tooltip title={t('paintings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/paintings')}>
<Icon className={isRoute('/paintings')}>
<PictureOutlined style={{ fontSize: 16 }} />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('translate.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/translate')}> <StyledLink onClick={() => to('/translate')}>
<Icon className={isRoute('/translate')}> <Icon className={isRoute('/translate')}>
<TranslationOutlined /> <TranslationOutlined />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/apps')}> <StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}> <Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore" /> <i className="iconfont icon-appstore" />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip>
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/files')}> <StyledLink onClick={() => to('/files')}>
<Icon className={isRoute('/files')}> <Icon className={isRoute('/files')}>
<FolderOutlined /> <FolderOutlined />
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink onClick={() => to('/messages')}> </Tooltip>
<Icon className={isRoutes('/messages')}>
<FileSearchOutlined />
</Icon>
</StyledLink>
</Menus> </Menus>
</MainMenus> </MainMenus>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
<Icon onClick={() => toggleTheme()}>
{theme === 'dark' ? (
<i className="iconfont icon-theme icon-dark1" />
) : (
<i className="iconfont icon-theme icon-theme-light" />
)}
</Icon>
</Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}> <StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}> <Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
<i className="iconfont icon-setting" /> <i className="iconfont icon-setting" />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip>
</Menus> </Menus>
</Container> </Container>
) )
@@ -101,7 +127,6 @@ const Container = styled.div`
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'}; height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
-webkit-app-region: drag !important; -webkit-app-region: drag !important;
margin-top: ${isMac ? 'var(--navbar-height)' : 0}; margin-top: ${isMac ? 'var(--navbar-height)' : 0};
transition: background-color 0.3s ease;
` `
const AvatarImg = styled(Avatar)` const AvatarImg = styled(Avatar)`
@@ -132,14 +157,12 @@ const Icon = styled.div`
align-items: center; align-items: center;
border-radius: 50%; border-radius: 50%;
margin-bottom: 5px; margin-bottom: 5px;
transition: background-color 0.2s ease;
-webkit-app-region: none; -webkit-app-region: none;
transition: all 0.2s ease; border: 0.5px solid transparent;
.iconfont, .iconfont,
.anticon { .anticon {
color: var(--color-icon); color: var(--color-icon);
font-size: 20px; font-size: 20px;
transition: color 0.2s ease;
text-decoration: none; text-decoration: none;
} }
.anticon { .anticon {
@@ -155,6 +178,7 @@ const Icon = styled.div`
} }
&.active { &.active {
background-color: var(--color-active); background-color: var(--color-active);
border: 0.5px solid var(--color-border);
.iconfont, .iconfont,
.anticon { .anticon {
color: var(--color-icon-white); color: var(--color-icon-white);

File diff suppressed because one or more lines are too long

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