Compare commits
422 Commits
v0.8.9
...
feat/setti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c66f0e41a | ||
|
|
fd1629e004 | ||
|
|
790caae2ab | ||
|
|
7f7300e6dc | ||
|
|
4464992873 | ||
|
|
37d1c250d2 | ||
|
|
e9c51579a2 | ||
|
|
aec2952780 | ||
|
|
95a1bdac72 | ||
|
|
306cb04ef0 | ||
|
|
dc9444a9d4 | ||
|
|
ad9fefe902 | ||
|
|
e07d4838a9 | ||
|
|
30d070040c | ||
|
|
f335699958 | ||
|
|
b1bc576e3f | ||
|
|
a6f086e3be | ||
|
|
084da9ebab | ||
|
|
57aef23741 | ||
|
|
900b11bdf7 | ||
|
|
8aec8a60b3 | ||
|
|
a566b0e91a | ||
|
|
4d201059ad | ||
|
|
00d91ecf01 | ||
|
|
462ac39897 | ||
|
|
3fa1e8c842 | ||
|
|
d32a76c087 | ||
|
|
9e9fd37bda | ||
|
|
dd464db594 | ||
|
|
ccac5358f4 | ||
|
|
e72e324155 | ||
|
|
28c18b6651 | ||
|
|
3d432d810f | ||
|
|
21ad28ee62 | ||
|
|
f7db1289e4 | ||
|
|
f5c547cdb2 | ||
|
|
9160cee919 | ||
|
|
298bb8be29 | ||
|
|
b800c64fed | ||
|
|
504d7b88d4 | ||
|
|
713d6dba8f | ||
|
|
a6833d5994 | ||
|
|
d850fd315a | ||
|
|
c04fd62bec | ||
|
|
f86a274cd3 | ||
|
|
798a6e8c3e | ||
|
|
749353f460 | ||
|
|
c510f5dcce | ||
|
|
46b314303c | ||
|
|
b01aca9066 | ||
|
|
725f81c165 | ||
|
|
c0e25879e5 | ||
|
|
4c22c404ca | ||
|
|
63673ec39f | ||
|
|
88cc783a95 | ||
|
|
9c55b4516c | ||
|
|
aecc5fefcf | ||
|
|
afc2e2f595 | ||
|
|
67b63ee07a | ||
|
|
fd7132cd3a | ||
|
|
a7d9700f06 | ||
|
|
d9bb552f3f | ||
|
|
ad2713c0be | ||
|
|
1e756614f9 | ||
|
|
d457dfa3d3 | ||
|
|
b24d88dfe3 | ||
|
|
b6d598c52e | ||
|
|
67e1dd56e9 | ||
|
|
8b5dd427d0 | ||
|
|
4f44afeec4 | ||
|
|
c46219cd6c | ||
|
|
999bd802c4 | ||
|
|
2300cca070 | ||
|
|
b4de6292c3 | ||
|
|
42908e8834 | ||
|
|
57718dda6f | ||
|
|
c87e88a53a | ||
|
|
5b00c21f15 | ||
|
|
6276890e5b | ||
|
|
a7337ed4b0 | ||
|
|
fe0f6318c9 | ||
|
|
75742323ea | ||
|
|
f7f8c6f0c6 | ||
|
|
e4f4c6cd86 | ||
|
|
8eac836e05 | ||
|
|
a6795289da | ||
|
|
eff639ddf9 | ||
|
|
a046cf32ba | ||
|
|
66bc9cb3f9 | ||
|
|
247d1a1846 | ||
|
|
0e7fb2b19c | ||
|
|
8a94bb05ea | ||
|
|
bc454d4dec | ||
|
|
d388aeecfb | ||
|
|
3e33ee6cc5 | ||
|
|
1991df18d2 | ||
|
|
de3206b052 | ||
|
|
cb3ed42846 | ||
|
|
edbc8560cc | ||
|
|
56761d6f69 | ||
|
|
2b4cfe7cb1 | ||
|
|
6a5faa6610 | ||
|
|
84979a975c | ||
|
|
74740d7fcc | ||
|
|
dff04187be | ||
|
|
a0a13a4015 | ||
|
|
2ad6a1f24c | ||
|
|
cf7c0fc1fc | ||
|
|
4ecbf3edab | ||
|
|
83cc4ccec7 | ||
|
|
3998ad08de | ||
|
|
49a5bc7900 | ||
|
|
7633d70435 | ||
|
|
ad9fb9aa6d | ||
|
|
fc3d15fae8 | ||
|
|
c45fc2bbad | ||
|
|
270216f461 | ||
|
|
112e90c15c | ||
|
|
c579eff86e | ||
|
|
f9f5befc59 | ||
|
|
7271a86677 | ||
|
|
42ede42f62 | ||
|
|
ea7a42f736 | ||
|
|
d2836826e7 | ||
|
|
7d61af7170 | ||
|
|
3f4fa9b0ec | ||
|
|
1bdf6c7955 | ||
|
|
5d005cf5a7 | ||
|
|
1fbd727a7b | ||
|
|
c9813bb1e2 | ||
|
|
edac2004a0 | ||
|
|
a051f9fa44 | ||
|
|
a70e69caf9 | ||
|
|
4896db93fd | ||
|
|
2e7ecbc753 | ||
|
|
f68bd4d8d8 | ||
|
|
d0948e6f8a | ||
|
|
ac9017c031 | ||
|
|
de1d79abb8 | ||
|
|
ad577818dd | ||
|
|
bb50447a98 | ||
|
|
158f9bf1ad | ||
|
|
6a9bc103d7 | ||
|
|
529ec3612e | ||
|
|
d241c38c61 | ||
|
|
ee5ed8c565 | ||
|
|
dc73661678 | ||
|
|
ce973ce3a0 | ||
|
|
a0413158c8 | ||
|
|
6cb3b16451 | ||
|
|
08b0990cf9 | ||
|
|
10b9940edd | ||
|
|
4cbdd563e8 | ||
|
|
dba1f76db7 | ||
|
|
15fb605eb4 | ||
|
|
1bf147fa6a | ||
|
|
a782b2b4aa | ||
|
|
7f92cb59a6 | ||
|
|
6009ae84fb | ||
|
|
038aa2d5cc | ||
|
|
6384525e20 | ||
|
|
3fc7911c97 | ||
|
|
5f55d8c22c | ||
|
|
d9f7bcfc21 | ||
|
|
aa72794967 | ||
|
|
09e6756efe | ||
|
|
dde0400f0d | ||
|
|
1d3a01dd49 | ||
|
|
63cdc15bc2 | ||
|
|
b2818f8619 | ||
|
|
8ef9fb0216 | ||
|
|
63488e6fab | ||
|
|
6d9013f0a1 | ||
|
|
1a68587684 | ||
|
|
47c455b125 | ||
|
|
96124cf58e | ||
|
|
ef975add01 | ||
|
|
ed49066bab | ||
|
|
e7545c5a94 | ||
|
|
fc35df65b8 | ||
|
|
56ca81d245 | ||
|
|
6bc1f4b640 | ||
|
|
ccb216e76a | ||
|
|
60931b85ff | ||
|
|
dc1dbc7bb6 | ||
|
|
5d2efbd62b | ||
|
|
5337017648 | ||
|
|
c409256ae9 | ||
|
|
4ac608052c | ||
|
|
5e6aaabb23 | ||
|
|
8812daeeee | ||
|
|
13e3a8478c | ||
|
|
8687985ccb | ||
|
|
7d54f9b4fa | ||
|
|
6b7ba35183 | ||
|
|
5b42a6d054 | ||
|
|
153e7a9299 | ||
|
|
77e0c5172e | ||
|
|
c50ac440c8 | ||
|
|
34ebab0af8 | ||
|
|
b85765915e | ||
|
|
960f50e4e4 | ||
|
|
65e19d187c | ||
|
|
aa4f94f8a4 | ||
|
|
aa3812eddc | ||
|
|
6b9e58171b | ||
|
|
2f64653b1e | ||
|
|
03dd3038e0 | ||
|
|
f1f7e8e11b | ||
|
|
fbd189c5e1 | ||
|
|
87c3716f75 | ||
|
|
37477587b6 | ||
|
|
d558572d97 | ||
|
|
7506d04c55 | ||
|
|
35fd5aef22 | ||
|
|
8f11d2b1c9 | ||
|
|
9aa2a4727d | ||
|
|
ca6027dd83 | ||
|
|
c2462fd51c | ||
|
|
0739758469 | ||
|
|
b2554333a9 | ||
|
|
6ced973b35 | ||
|
|
ccbeefc546 | ||
|
|
7fdc2db522 | ||
|
|
978f1342e4 | ||
|
|
ff935a656e | ||
|
|
15539a5609 | ||
|
|
88cd4f2144 | ||
|
|
daf2e035b2 | ||
|
|
7ceb4920ec | ||
|
|
0074d5c8b4 | ||
|
|
96737ed695 | ||
|
|
356da1ea67 | ||
|
|
debf996146 | ||
|
|
8d73d1e844 | ||
|
|
b0d777293b | ||
|
|
1a9fbbc0a2 | ||
|
|
ab99a7b96d | ||
|
|
7d561dbfb7 | ||
|
|
6af07c278d | ||
|
|
9c18b851cc | ||
|
|
b1ebe13b5f | ||
|
|
9b258734c4 | ||
|
|
25eb97902b | ||
|
|
2fae6e4a3e | ||
|
|
f312c5fc40 | ||
|
|
afa96549a3 | ||
|
|
6beee78ce8 | ||
|
|
a230ee2c69 | ||
|
|
28a27447a5 | ||
|
|
408976e5dc | ||
|
|
7153996c35 | ||
|
|
73f6a743cd | ||
|
|
3b250d7d78 | ||
|
|
272efaf76e | ||
|
|
44c64a571a | ||
|
|
f817d9136b | ||
|
|
c0f192c6f2 | ||
|
|
b5a109401c | ||
|
|
aeff59946c | ||
|
|
21ad4cfecc | ||
|
|
4df39179bb | ||
|
|
423fdb6992 | ||
|
|
f66adcd217 | ||
|
|
465bf4006c | ||
|
|
14c9cb6001 | ||
|
|
e35d928bcd | ||
|
|
1981f2e648 | ||
|
|
e5c1791135 | ||
|
|
ae1960f5c6 | ||
|
|
51ca9cb289 | ||
|
|
7d2df1a8c5 | ||
|
|
2757535cf0 | ||
|
|
243065221d | ||
|
|
2a674c169e | ||
|
|
100dbc8101 | ||
|
|
67d7ccbf10 | ||
|
|
a71782abb6 | ||
|
|
73973ecb7f | ||
|
|
368de84440 | ||
|
|
a170dbd6f0 | ||
|
|
9b84176a42 | ||
|
|
0f36610e23 | ||
|
|
1e273834b8 | ||
|
|
3b569131a5 | ||
|
|
115f111071 | ||
|
|
a4d1bcffd9 | ||
|
|
f5d37a4e53 | ||
|
|
a9d4a0885c | ||
|
|
6596497c97 | ||
|
|
12d8f57dab | ||
|
|
7f2f3ad88a | ||
|
|
cd3c053f81 | ||
|
|
7dacd58821 | ||
|
|
744a6ac7cb | ||
|
|
2e9041c891 | ||
|
|
3717ff25bf | ||
|
|
494d52ac85 | ||
|
|
22d2ff1518 | ||
|
|
06ae4328ea | ||
|
|
8de1197557 | ||
|
|
09e86b35a5 | ||
|
|
76ea170a01 | ||
|
|
4cd962b42f | ||
|
|
1cae86f93d | ||
|
|
1171100417 | ||
|
|
dcf57651fe | ||
|
|
603b867a5f | ||
|
|
e765bf9828 | ||
|
|
33d5da7325 | ||
|
|
383e8255a0 | ||
|
|
4cb5c128bb | ||
|
|
949a13b021 | ||
|
|
e6c9cb60dc | ||
|
|
ad625b23a7 | ||
|
|
2e34b79f26 | ||
|
|
9288e7b292 | ||
|
|
ec703852f8 | ||
|
|
fca9fb0c84 | ||
|
|
64c8831530 | ||
|
|
75e396ecf0 | ||
|
|
697c3b1838 | ||
|
|
efa0f4cbdb | ||
|
|
c3414e9b6d | ||
|
|
6ba6108f43 | ||
|
|
c3d007b52c | ||
|
|
e1494d408f | ||
|
|
cd625430b2 | ||
|
|
aefb08965d | ||
|
|
d2dd70000b | ||
|
|
f0a96bb34c | ||
|
|
0ec61e1c47 | ||
|
|
335ce4963b | ||
|
|
63ef0d2df1 | ||
|
|
c0c0e8ae33 | ||
|
|
771b078df9 | ||
|
|
64e70ea918 | ||
|
|
2d46a4494e | ||
|
|
9b7e2282fe | ||
|
|
535b7d0a92 | ||
|
|
223496192d | ||
|
|
db779446f0 | ||
|
|
8ef3ef2a8f | ||
|
|
30da183578 | ||
|
|
49c09f381c | ||
|
|
c8c58ddcfb | ||
|
|
5bffb86d4f | ||
|
|
84fa5b065b | ||
|
|
7ecb35dfa7 | ||
|
|
2d7d403b15 | ||
|
|
7342a0afef | ||
|
|
1b8a3885f7 | ||
|
|
c33c0b20f2 | ||
|
|
4f75f29361 | ||
|
|
fe00eed7b9 | ||
|
|
30fa9277ff | ||
|
|
11a446e106 | ||
|
|
d2ca6f1041 | ||
|
|
0f3dc87d08 | ||
|
|
7d3cae1f5b | ||
|
|
ceae1fa3d0 | ||
|
|
12a2c8c86d | ||
|
|
29d6c4be18 | ||
|
|
738e51c078 | ||
|
|
db050c002a | ||
|
|
398f995cd1 | ||
|
|
348fc365fa | ||
|
|
7b6d38e349 | ||
|
|
6a35c0e3d8 | ||
|
|
9a63169a73 | ||
|
|
a9aa5a8da0 | ||
|
|
a2d568175b | ||
|
|
0b9717780d | ||
|
|
b371fed814 | ||
|
|
3311f8cdef | ||
|
|
422baa848b | ||
|
|
739aa21475 | ||
|
|
23ef4ab952 | ||
|
|
b77f845cb0 | ||
|
|
0573b274ed | ||
|
|
60433bb1ab | ||
|
|
1caf53fbda | ||
|
|
446e011c6a | ||
|
|
c319b54a26 | ||
|
|
e9ca1d54a0 | ||
|
|
1ff8fe0c2e | ||
|
|
902341bc1d | ||
|
|
17fff46024 | ||
|
|
d258c1cfe2 | ||
|
|
79cabadfb8 | ||
|
|
61ceca2363 | ||
|
|
8a8deda002 | ||
|
|
6536ec227a | ||
|
|
af1fd90118 | ||
|
|
68fa2bad15 | ||
|
|
bac3bad8db | ||
|
|
e11633310c | ||
|
|
612b39a878 | ||
|
|
8491141edc | ||
|
|
088628f89f | ||
|
|
a6b4e48640 | ||
|
|
ba0e2c5848 | ||
|
|
d986087857 | ||
|
|
73c93c5581 | ||
|
|
ceec4a9f97 | ||
|
|
cf08467552 | ||
|
|
34c85e8f0c | ||
|
|
1db3faa2a8 | ||
|
|
35efada37e | ||
|
|
ceca3408ff | ||
|
|
f2def559d4 | ||
|
|
cd97be0f10 | ||
|
|
b87394ed88 | ||
|
|
a4d8e71916 | ||
|
|
39cf227e42 | ||
|
|
5d01d12d2a | ||
|
|
d2cad31db4 | ||
|
|
d7f4e4584a | ||
|
|
eda870f181 | ||
|
|
3f093a91be | ||
|
|
aa864f3876 | ||
|
|
77a8b23d76 |
@@ -2,4 +2,4 @@ node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
|
||||
scripts/cloudflare-worker.js
|
||||
|
||||
73
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: 🐛 错误报告
|
||||
description: 创建一个报告以帮助我们改进
|
||||
title: '[错误]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间填写此错误报告!
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台
|
||||
description: 您正在使用哪个平台?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||
placeholder: 例如 v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 错误描述
|
||||
description: 清晰简洁地描述错误是什么
|
||||
placeholder: 告诉我们发生了什么...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: 重现行为的步骤
|
||||
placeholder: |
|
||||
1. 转到 '...'
|
||||
2. 点击 '....'
|
||||
3. 向下滚动到 '....'
|
||||
4. 看到错误
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 预期行为
|
||||
description: 清晰简洁地描述您期望发生的事情
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 相关日志输出
|
||||
description: 请复制并粘贴任何相关的日志输出
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 附加信息
|
||||
description: 在此添加有关问题的任何其他上下文
|
||||
38
.github/ISSUE_TEMPLATE/#1_feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: 💡 功能建议
|
||||
description: 为项目提出新的想法
|
||||
title: '[功能]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间提出新的功能建议!
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 您的功能建议是否与某个问题相关?
|
||||
description: 请简明扼要地描述您遇到的问题
|
||||
placeholder: 我总是感到沮丧,因为...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 请描述您希望实现的解决方案
|
||||
description: 请简明扼要地描述您希望发生的情况
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 请描述您考虑过的其他方案
|
||||
description: 请简明扼要地描述您考虑过的任何其他解决方案或功能
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 其他补充信息
|
||||
description: 在此添加任何其他与功能建议相关的上下文或截图
|
||||
44
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: ❓ 提问
|
||||
description: 提出一个问题或寻求帮助
|
||||
title: '[问题]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的提问!请尽可能详细地描述您的问题,这样我们才能更好地帮助您。
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: 您的问题
|
||||
description: 请详细描述您的问题
|
||||
placeholder: 请尽可能清楚地说明您的问题...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 相关背景
|
||||
description: 请提供一些背景信息,帮助我们更好地理解您的问题
|
||||
placeholder: 例如:使用场景、已尝试的解决方案等
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 任何其他相关的信息、截图或代码示例
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级
|
||||
description: 这个问题对您来说有多紧急?
|
||||
options:
|
||||
- 低 (有空再看)
|
||||
- 中 (希望尽快得到答复)
|
||||
- 高 (阻碍工作进行)
|
||||
validations:
|
||||
required: true
|
||||
73
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: What platform are you using?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Cherry Studio are you running?
|
||||
placeholder: e.g. v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is
|
||||
placeholder: Tell us what happened...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant Log Output
|
||||
description: Please copy and paste any relevant log output
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here
|
||||
38
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to suggest a new feature!
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: A clear and concise description of what the problem is
|
||||
placeholder: I'm always frustrated when...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here
|
||||
44
.github/ISSUE_TEMPLATE/2_question.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: ❓ Question
|
||||
description: Ask a question or seek help
|
||||
title: '[Question]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for asking a question! Please provide as much detail as possible so we can better assist you.
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Your Question
|
||||
description: Please describe your question in detail
|
||||
placeholder: Please explain your question as clearly as possible...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Context
|
||||
description: Please provide some background information to help us better understand your question
|
||||
placeholder: "For example: use case, solutions you've tried, etc."
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other relevant information, screenshots, or code examples
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How urgent is this question for you?
|
||||
options:
|
||||
- Low (Can wait)
|
||||
- Medium (Would like a response soon)
|
||||
- High (Blocking progress)
|
||||
validations:
|
||||
required: true
|
||||
45
.github/workflows/release.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
@@ -28,18 +29,37 @@ jobs:
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.3.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache yarn dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: yarn build:linux
|
||||
run: |
|
||||
yarn build:npm linux
|
||||
yarn build:linux
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: yarn build:mac
|
||||
run: |
|
||||
yarn build:npm mac
|
||||
yarn build:mac
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
@@ -55,22 +75,13 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Replace spaces in filenames
|
||||
run: node scripts/replaceSpaces.js
|
||||
run: node scripts/replace-spaces.js
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.zip
|
||||
dist/*.dmg
|
||||
dist/*.AppImage
|
||||
dist/*.snap
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
dist/*.tar.gz
|
||||
dist/latest*.yml
|
||||
dist/*.blockmap
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
allowUpdates: true
|
||||
makeLatest: false
|
||||
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/*.blockmap'
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
@@ -36,6 +36,7 @@ node_modules
|
||||
dist
|
||||
out
|
||||
build/icons
|
||||
stats.html
|
||||
|
||||
# ENV
|
||||
.env
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
diff --git a/lib/check-signature.js b/lib/check-signature.js
|
||||
index 324568af71bcc4372c9f959131ecd24122848c86..677348e0a138ff608b2ac41f592d813b15ee4956 100644
|
||||
--- a/lib/check-signature.js
|
||||
+++ b/lib/check-signature.js
|
||||
@@ -41,16 +41,12 @@ const spawn_1 = require("./spawn");
|
||||
const debug_1 = __importDefault(require("debug"));
|
||||
const d = (0, debug_1.default)('electron-notarize');
|
||||
const codesignDisplay = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
- const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', opts.appPath]);
|
||||
return result;
|
||||
});
|
||||
const codesign = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
d('attempting to check codesign of app:', opts.appPath);
|
||||
- const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', opts.appPath]);
|
||||
return result;
|
||||
});
|
||||
function checkSignatures(opts) {
|
||||
diff --git a/lib/notarytool.js b/lib/notarytool.js
|
||||
index 1ab090efb2101fc8bee5553445e0349c54474421..a5ddfd922197449fc56078e4a7e9a2ee5d8d207d 100644
|
||||
--- a/lib/notarytool.js
|
||||
+++ b/lib/notarytool.js
|
||||
@@ -92,9 +92,7 @@ function notarizeAndWaitForNotaryTool(opts) {
|
||||
else {
|
||||
filePath = path.resolve(dir, `${path.parse(opts.appPath).name}.zip`);
|
||||
d('zipping application to:', filePath);
|
||||
- const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), filePath], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', opts.appPath, filePath]);
|
||||
if (zipResult.code !== 0) {
|
||||
throw new Error(`Failed to zip application, exited with code: ${zipResult.code}\n\n${zipResult.output}`);
|
||||
}
|
||||
diff --git a/lib/staple.js b/lib/staple.js
|
||||
index 47dbd85b2fc279d999b57f47fb8171e1cc674436..f8829e6ac54fcd630a730d12d75acc1591b953b6 100644
|
||||
--- a/lib/staple.js
|
||||
+++ b/lib/staple.js
|
||||
@@ -43,9 +43,7 @@ const d = (0, debug_1.default)('electron-notarize:staple');
|
||||
function stapleApp(opts) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
d('attempting to staple app:', opts.appPath);
|
||||
- const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', opts.appPath]);
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`Failed to staple your application with code: ${result.code}\n\n${result.output}`);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
diff --git a/src/libsql-db.js b/src/libsql-db.js
|
||||
index 58c42e4910bd0e53bc497ff9b9702b1f7a961266..250bc97c50a9b790e8798441d904d040f2d2af43 100644
|
||||
--- a/src/libsql-db.js
|
||||
+++ b/src/libsql-db.js
|
||||
@@ -41,9 +41,9 @@ export class LibSqlDb {
|
||||
}
|
||||
async similaritySearch(query, k) {
|
||||
const statement = `SELECT id, pageContent, uniqueLoaderId, source, metadata,
|
||||
- vector_distance_cos(vector, vector32('[${query.join(',')}]'))
|
||||
+ vector_distance_cos(vector, vector32('[${query.join(',')}]')) as distance
|
||||
FROM ${this.tableName}
|
||||
- ORDER BY vector_distance_cos(vector, vector32('[${query.join(',')}]')) ASC
|
||||
+ ORDER BY distance ASC
|
||||
LIMIT ${k};`;
|
||||
this.debug(`Executing statement - ${truncateCenterString(statement, 700)}`);
|
||||
const results = await this.client.execute(statement);
|
||||
@@ -52,7 +52,7 @@ export class LibSqlDb {
|
||||
return {
|
||||
metadata,
|
||||
pageContent: result.pageContent.toString(),
|
||||
- score: 1,
|
||||
+ score: 1 - result.distance,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
|
||||
index 8a17cb7f5a68d90d2be21682db6e95ce22a3e71c..9ee868ef9d4ff3dc914b3abc3c8006deb1e9c6c6 100644
|
||||
--- a/src/markdown-loader.js
|
||||
+++ b/src/markdown-loader.js
|
||||
@@ -1,5 +1,4 @@
|
||||
import { micromark } from 'micromark';
|
||||
-import { mdxJsx } from 'micromark-extension-mdx-jsx';
|
||||
import { gfmHtml, gfm } from 'micromark-extension-gfm';
|
||||
import createDebugMessages from 'debug';
|
||||
import fs from 'node:fs';
|
||||
@@ -21,7 +20,7 @@ export class MarkdownLoader extends BaseLoader {
|
||||
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
|
||||
: await stream2buffer(fs.createReadStream(this.filePathOrUrl));
|
||||
this.debug('MarkdownLoader stream created');
|
||||
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
|
||||
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
|
||||
this.debug('Markdown parsed...');
|
||||
const webLoader = new WebLoader({
|
||||
urlOrContent: result,
|
||||
217
.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch
Normal file
@@ -0,0 +1,217 @@
|
||||
diff --git a/src/core/rag-embedding.js b/src/core/rag-embedding.js
|
||||
index 50c3c4064af17bc4c7c46554d8f2419b3afceb0e..632c9b2e04d2e0e3bb09ef1cd8f29d2560e6afc1 100644
|
||||
--- a/src/core/rag-embedding.js
|
||||
+++ b/src/core/rag-embedding.js
|
||||
@@ -1,10 +1,8 @@
|
||||
export class RAGEmbedding {
|
||||
static singleton;
|
||||
static async init(embeddingModel) {
|
||||
- if (!this.singleton) {
|
||||
- await embeddingModel.init();
|
||||
- this.singleton = new RAGEmbedding(embeddingModel);
|
||||
- }
|
||||
+ await embeddingModel.init();
|
||||
+ this.singleton = new RAGEmbedding(embeddingModel);
|
||||
}
|
||||
static getInstance() {
|
||||
return RAGEmbedding.singleton;
|
||||
diff --git a/src/loaders/local-path-loader.d.ts b/src/loaders/local-path-loader.d.ts
|
||||
index 48c20e68c469cd309be2dc8f28e44c1bd04a26e9..87002be39e7305a02e2a607b0c0d95cbbc359f9d 100644
|
||||
--- a/src/loaders/local-path-loader.d.ts
|
||||
+++ b/src/loaders/local-path-loader.d.ts
|
||||
@@ -1,19 +1,29 @@
|
||||
-import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
+import { BaseLoader } from "@llm-tools/embedjs-interfaces";
|
||||
export declare class LocalPathLoader extends BaseLoader<{
|
||||
- type: 'LocalPathLoader';
|
||||
+ type: "LocalPathLoader";
|
||||
}> {
|
||||
- private readonly debug;
|
||||
- private readonly path;
|
||||
- constructor({ path }: {
|
||||
- path: string;
|
||||
- });
|
||||
- getUnfilteredChunks(): AsyncGenerator<{
|
||||
- metadata: {
|
||||
- type: "LocalPathLoader";
|
||||
- originalPath: string;
|
||||
- source: string;
|
||||
- };
|
||||
- pageContent: string;
|
||||
- }, void, unknown>;
|
||||
- private recursivelyAddPath;
|
||||
+ private readonly debug;
|
||||
+ private readonly path;
|
||||
+ constructor({
|
||||
+ path,
|
||||
+ chunkSize,
|
||||
+ chunkOverlap,
|
||||
+ }: {
|
||||
+ path: string;
|
||||
+ chunkSize?: number;
|
||||
+ chunkOverlap?: number;
|
||||
+ });
|
||||
+ getUnfilteredChunks(): AsyncGenerator<
|
||||
+ {
|
||||
+ metadata: {
|
||||
+ type: "LocalPathLoader";
|
||||
+ originalPath: string;
|
||||
+ source: string;
|
||||
+ };
|
||||
+ pageContent: string;
|
||||
+ },
|
||||
+ void,
|
||||
+ unknown
|
||||
+ >;
|
||||
+ private recursivelyAddPath;
|
||||
}
|
||||
diff --git a/src/loaders/local-path-loader.js b/src/loaders/local-path-loader.js
|
||||
index 4cf8a6bd1d890244c8ec49d4a05ee3bd58861c79..fd0fe1951c73da315b0c9bf4a8f33effbadb9f8f 100644
|
||||
--- a/src/loaders/local-path-loader.js
|
||||
+++ b/src/loaders/local-path-loader.js
|
||||
@@ -8,8 +8,8 @@ import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
export class LocalPathLoader extends BaseLoader {
|
||||
debug = createDebugMessages('embedjs:loader:LocalPathLoader');
|
||||
path;
|
||||
- constructor({ path }) {
|
||||
- super(`LocalPathLoader_${md5(path)}`, { path });
|
||||
+ constructor({ path, chunkSize, chunkOverlap}) {
|
||||
+ super(`LocalPathLoader_${md5(path)}`, { path }, chunkSize ?? 1000, chunkOverlap ?? 0);
|
||||
this.path = path;
|
||||
}
|
||||
async *getUnfilteredChunks() {
|
||||
@@ -36,10 +36,12 @@ export class LocalPathLoader extends BaseLoader {
|
||||
const extension = currentPath.split('.').pop().toLowerCase();
|
||||
if (extension === 'md' || extension === 'mdx')
|
||||
mime = 'text/markdown';
|
||||
+ if (extension === 'txt')
|
||||
+ mime = 'text/plain';
|
||||
this.debug(`File '${this.path}' mime type updated to 'text/markdown'`);
|
||||
}
|
||||
try {
|
||||
- const loader = await createLoaderFromMimeType(currentPath, mime);
|
||||
+ const loader = await createLoaderFromMimeType(currentPath, mime, this.chunkSize, this.chunkOverlap);
|
||||
for await (const result of await loader.getUnfilteredChunks()) {
|
||||
yield {
|
||||
pageContent: result.pageContent,
|
||||
diff --git a/src/util/mime.d.ts b/src/util/mime.d.ts
|
||||
index 57f56a1b8edc98366af9f84d671676c41c2f01ca..f53856fa9c78afbeee9e085c7ed0b3a131f8ee5a 100644
|
||||
--- a/src/util/mime.d.ts
|
||||
+++ b/src/util/mime.d.ts
|
||||
@@ -1,2 +1,7 @@
|
||||
-import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
-export declare function createLoaderFromMimeType(loaderData: string, mimeType: string): Promise<BaseLoader>;
|
||||
+import { BaseLoader } from "@llm-tools/embedjs-interfaces";
|
||||
+export declare function createLoaderFromMimeType(
|
||||
+ loaderData: string,
|
||||
+ mimeType: string,
|
||||
+ chunkSize?: number,
|
||||
+ chunkOverlap?: number
|
||||
+): Promise<BaseLoader>;
|
||||
diff --git a/src/util/mime.js b/src/util/mime.js
|
||||
index 9af30bd5b8cf42985f547073a4c19756292c33a3..54ae20343131a533ab70236d3060b6accc8f6126 100644
|
||||
--- a/src/util/mime.js
|
||||
+++ b/src/util/mime.js
|
||||
@@ -1,7 +1,9 @@
|
||||
import mime from 'mime';
|
||||
import createDebugMessages from 'debug';
|
||||
import { TextLoader } from '../loaders/text-loader.js';
|
||||
-export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
+import fs from 'node:fs';
|
||||
+
|
||||
+export async function createLoaderFromMimeType(loaderData, mimeType, chunkSize, chunkOverlap) {
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')(`Incoming mime type '${mimeType}'`);
|
||||
switch (mimeType) {
|
||||
case 'application/msword':
|
||||
@@ -10,7 +12,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load docx files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported DocxLoader');
|
||||
- return new DocxLoader({ filePathOrUrl: loaderData });
|
||||
+ return new DocxLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/vnd.ms-excel':
|
||||
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
|
||||
@@ -18,21 +20,21 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load excel files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported ExcelLoader');
|
||||
- return new ExcelLoader({ filePathOrUrl: loaderData });
|
||||
+ return new ExcelLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/pdf': {
|
||||
const { PdfLoader } = await import('@llm-tools/embedjs-loader-pdf').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-pdf` needs to be installed to load PDF files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PdfLoader');
|
||||
- return new PdfLoader({ filePathOrUrl: loaderData });
|
||||
+ return new PdfLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': {
|
||||
const { PptLoader } = await import('@llm-tools/embedjs-loader-msoffice').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load pptx files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PptLoader');
|
||||
- return new PptLoader({ filePathOrUrl: loaderData });
|
||||
+ return new PptLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/plain': {
|
||||
const fineType = mime.getType(loaderData);
|
||||
@@ -42,24 +44,26 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
|
||||
- return new CsvLoader({ filePathOrUrl: loaderData });
|
||||
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
+ }
|
||||
+ else{
|
||||
+ const content = fs.readFileSync(loaderData, 'utf-8');
|
||||
+ return new TextLoader({ text: content, chunkSize, chunkOverlap });
|
||||
}
|
||||
- else
|
||||
- return new TextLoader({ text: loaderData });
|
||||
}
|
||||
case 'application/csv': {
|
||||
const { CsvLoader } = await import('@llm-tools/embedjs-loader-csv').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
|
||||
- return new CsvLoader({ filePathOrUrl: loaderData });
|
||||
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/html': {
|
||||
const { WebLoader } = await import('@llm-tools/embedjs-loader-web').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-web` needs to be installed to load web documents');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported WebLoader');
|
||||
- return new WebLoader({ urlOrContent: loaderData });
|
||||
+ return new WebLoader({ urlOrContent: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/xml': {
|
||||
const { SitemapLoader } = await import('@llm-tools/embedjs-loader-sitemap').catch(() => {
|
||||
@@ -67,14 +71,14 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported SitemapLoader');
|
||||
if (await SitemapLoader.test(loaderData)) {
|
||||
- return new SitemapLoader({ url: loaderData });
|
||||
+ return new SitemapLoader({ url: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
//This is not a Sitemap but is still XML
|
||||
const { XmlLoader } = await import('@llm-tools/embedjs-loader-xml').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-xml` needs to be installed to load XML documents');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported XmlLoader');
|
||||
- return new XmlLoader({ filePathOrUrl: loaderData });
|
||||
+ return new XmlLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/x-markdown':
|
||||
case 'text/markdown': {
|
||||
@@ -82,7 +86,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-markdown` needs to be installed to load markdown files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported MarkdownLoader');
|
||||
- return new MarkdownLoader({ filePathOrUrl: loaderData });
|
||||
+ return new MarkdownLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case undefined:
|
||||
throw new Error(`MIME type could not be detected. Please file an issue if you think this is a bug.`);
|
||||
@@ -0,0 +1,54 @@
|
||||
diff --git a/src/util/strings.cjs b/src/util/strings.cjs
|
||||
index 9933cc6e3866c476b47342a29ddb206eb90fa4a5..2965c4f2808bf94af9ef3e2ec889e5552e30e6ae 100644
|
||||
--- a/src/util/strings.cjs
|
||||
+++ b/src/util/strings.cjs
|
||||
@@ -38,13 +38,16 @@ function toTitleCase(str) {
|
||||
});
|
||||
}
|
||||
function isValidURL(url) {
|
||||
- try {
|
||||
- new URL(url);
|
||||
- return true;
|
||||
- }
|
||||
- catch {
|
||||
- return false;
|
||||
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) {
|
||||
+ try {
|
||||
+ new URL(url);
|
||||
+ return true;
|
||||
+ }
|
||||
+ catch {
|
||||
+ return false;
|
||||
+ }
|
||||
}
|
||||
+ return false;
|
||||
}
|
||||
function isValidJson(str) {
|
||||
try {
|
||||
diff --git a/src/util/strings.js b/src/util/strings.js
|
||||
index f5c1655512099b880fc5022e95d5e0c4d1d073f2..1a64bd662a22efd2effd9d2846ffcf0b93391963 100644
|
||||
--- a/src/util/strings.js
|
||||
+++ b/src/util/strings.js
|
||||
@@ -29,13 +29,16 @@ export function toTitleCase(str) {
|
||||
});
|
||||
}
|
||||
export function isValidURL(url) {
|
||||
- try {
|
||||
- new URL(url);
|
||||
- return true;
|
||||
- }
|
||||
- catch {
|
||||
- return false;
|
||||
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) {
|
||||
+ try {
|
||||
+ new URL(url);
|
||||
+ return true;
|
||||
+ }
|
||||
+ catch {
|
||||
+ return false;
|
||||
+ }
|
||||
}
|
||||
+ return false;
|
||||
}
|
||||
export function isValidJson(str) {
|
||||
try {
|
||||
26
.yarn/patches/openai-npm-4.76.2-8ff1374617.patch
Normal file
@@ -0,0 +1,26 @@
|
||||
diff --git a/core.js b/core.js
|
||||
index 30c91e66bf595a66c09eb3dbcbda7d58154865f5..b511ff24ea1891904c60174c6ed26ecdd4d5ac51 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 ac267bcfcff44b1f7c9bea5513bba94726a31795..dd5bd9f29609d3f0eea4bd5b225f302893df14ad 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
.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch
Normal file
@@ -0,0 +1,29 @@
|
||||
diff --git a/lib/pdf-parse.js b/lib/pdf-parse.js
|
||||
index 96bfbc705dcb4fb73cb077a75f02c115371b3477..6d02d2bb426063c3a31cb740c3d86841de162a22 100644
|
||||
--- a/lib/pdf-parse.js
|
||||
+++ b/lib/pdf-parse.js
|
||||
@@ -21,12 +21,12 @@ function render_page(pageData) {
|
||||
for (let item of textContent.items) {
|
||||
if (lastY == item.transform[5] || !lastY){
|
||||
text += item.str;
|
||||
- }
|
||||
+ }
|
||||
else{
|
||||
text += '\n' + item.str;
|
||||
- }
|
||||
+ }
|
||||
lastY = item.transform[5];
|
||||
- }
|
||||
+ }
|
||||
//let strings = textContent.items.map(item => item.str);
|
||||
//let text = strings.join("\n");
|
||||
//text = text.replace(/[ ]+/ig," ");
|
||||
@@ -60,7 +60,7 @@ async function PDF(dataBuffer, options) {
|
||||
if (typeof options.version != 'string') options.version = DEFAULT_OPTIONS.version;
|
||||
if (options.version == 'default') options.version = DEFAULT_OPTIONS.version;
|
||||
|
||||
- PDFJS = PDFJS ? PDFJS : require(`./pdf.js/${options.version}/build/pdf.js`);
|
||||
+ PDFJS = PDFJS ? PDFJS : require(`./pdf.js/v1.10.100/build/pdf.js`);
|
||||
|
||||
ret.version = PDFJS.version;
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
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)
|
||||
👏 Join [Telegram Group](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/C3xrXWjY) | [QQ Group](https://qm.qq.com/q/pQPuHMjUeQ)
|
||||
|
||||
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
|
||||
|
||||
# 🌠 Screenshot
|
||||
|
||||
@@ -23,6 +23,8 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
||||
|
||||
# 🌟 Key Features
|
||||
|
||||

|
||||
|
||||
1. **Diverse LLM Provider Support**:
|
||||
|
||||
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
||||
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 41 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 137 KiB |
BIN
build/logo.png
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 84 KiB |
47
build/nsis-installer.nsh
Normal file
@@ -0,0 +1,47 @@
|
||||
;Inspired by:
|
||||
; https://gist.github.com/bogdibota/062919938e1ed388b3db5ea31f52955c
|
||||
; https://stackoverflow.com/questions/34177547/detect-if-visual-c-redistributable-for-visual-studio-2013-is-installed
|
||||
; https://stackoverflow.com/a/54391388
|
||||
; https://github.com/GitCommons/cpp-redist-nsis/blob/main/installer.nsh
|
||||
|
||||
;Find latests downloads here:
|
||||
; https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
|
||||
|
||||
!include LogicLib.nsh
|
||||
|
||||
; https://github.com/electron-userland/electron-builder/issues/1122
|
||||
!ifndef BUILD_UNINSTALLER
|
||||
Function checkVCRedist
|
||||
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
|
||||
FunctionEnd
|
||||
!endif
|
||||
|
||||
!macro customInit
|
||||
Push $0
|
||||
Call checkVCRedist
|
||||
${If} $0 != "1"
|
||||
MessageBox MB_YESNO "\
|
||||
NOTE: ${PRODUCT_NAME} requires $\r$\n\
|
||||
'Microsoft Visual C++ Redistributable'$\r$\n\
|
||||
to function properly.$\r$\n$\r$\n\
|
||||
Download and install now?" /SD IDYES IDYES InstallVCRedist IDNO DontInstall
|
||||
InstallVCRedist:
|
||||
inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable..." "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe"
|
||||
ExecWait "$TEMP\vc_redist.x64.exe /install /norestart"
|
||||
;IfErrors InstallError ContinueInstall ; vc_redist exit code is unreliable :(
|
||||
Call checkVCRedist
|
||||
${If} $0 == "1"
|
||||
Goto ContinueInstall
|
||||
${EndIf}
|
||||
|
||||
;InstallError:
|
||||
MessageBox MB_ICONSTOP "\
|
||||
There was an unexpected error installing$\r$\n\
|
||||
Microsoft Visual C++ Redistributable.$\r$\n\
|
||||
The installation of ${PRODUCT_NAME} cannot continue."
|
||||
DontInstall:
|
||||
Abort
|
||||
${EndIf}
|
||||
ContinueInstall:
|
||||
Pop $0
|
||||
!macroend
|
||||
BIN
build/tray_icon.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
build/tray_icon_dark.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
build/tray_icon_light.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,6 +1,8 @@
|
||||
# provider: generic
|
||||
# url: http://127.0.0.1:8080
|
||||
# updaterCacheDirName: cherry-studio-updater
|
||||
provider: github
|
||||
repo: cherry-studio
|
||||
owner: kangfenmao
|
||||
# provider: github
|
||||
# repo: cherry-studio
|
||||
# owner: kangfenmao
|
||||
provider: generic
|
||||
url: https://cherrystudio.ocool.online
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。
|
||||
|
||||
👏 [Telegramグループ](https://t.me/CherryStudioAI)に参加しましょう
|
||||
👏 [Telegram](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/C3xrXWjY) | [QQグループ](https://qm.qq.com/q/pQPuHMjUeQ)
|
||||
|
||||
❤️ Cherry Studioをお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
|
||||
|
||||
# 🌠 スクリーンショット
|
||||
|
||||
@@ -23,6 +23,8 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
|
||||
|
||||
# 🌟 主な機能
|
||||
|
||||

|
||||
|
||||
1. **多様な LLM サービス対応**:
|
||||
|
||||
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
|
||||
@@ -133,7 +135,7 @@ Cherry Studioへの貢献を歓迎します!以下の方法で貢献できま
|
||||
|
||||
# 📃 ライセンス
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
[LICENSE](../LICENSE)
|
||||
|
||||
# ⭐️ スター履歴
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。
|
||||
|
||||
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)
|
||||
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/C3xrXWjY) | [QQ 群](https://qm.qq.com/q/pQPuHMjUeQ)
|
||||
|
||||
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
|
||||
|
||||
# 🌠 界面
|
||||
|
||||
@@ -23,6 +23,8 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
||||
|
||||
# 🌟 主要特性
|
||||
|
||||

|
||||
|
||||
1. **多样化 LLM 服务支持**:
|
||||
|
||||
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||
@@ -134,7 +136,7 @@ $ yarn build:linux
|
||||
|
||||
# 📃 许可证
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
[LICENSE](../LICENSE)
|
||||
|
||||
# ⭐️ Star 记录
|
||||
|
||||
|
||||
@@ -11,10 +11,31 @@ files:
|
||||
- '!src'
|
||||
- '!scripts'
|
||||
- '!local'
|
||||
- '!docs'
|
||||
- '!packages'
|
||||
- '!stats.html'
|
||||
- '!*.md'
|
||||
- '!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}'
|
||||
- '!**/{test,tests,__tests__,coverage}/**'
|
||||
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
|
||||
- '!**/*.min.*.map'
|
||||
- '!**/*.d.ts'
|
||||
- '!**/{.DS_Store,Thumbs.db}'
|
||||
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
|
||||
- '!node_modules/rollup-plugin-visualizer'
|
||||
- '!node_modules/js-tiktoken'
|
||||
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
|
||||
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
|
||||
- '!node_modules/html2canvas/dist/{html2canvas.min.js,html2canvas.esm.js}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
- '**/*.{node,dll,metal,exp,lib}'
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-portable.${ext}
|
||||
target:
|
||||
- target: nsis
|
||||
- target: portable
|
||||
nsis:
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
@@ -22,14 +43,16 @@ nsis:
|
||||
createDesktopShortcut: always
|
||||
allowToChangeInstallationDirectory: true
|
||||
oneClick: false
|
||||
include: build/nsis-installer.nsh
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
notarize: false
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
@@ -39,30 +62,22 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
dmg:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
linux:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
target:
|
||||
- target: AppImage
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
# - snap
|
||||
# - deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: github
|
||||
repo: cherry-studio
|
||||
owner: kangfenmao
|
||||
provider: generic
|
||||
url: https://cherrystudio.ocool.online
|
||||
electronDownload:
|
||||
mirror: https://npmmirror.com/mirrors/electron/
|
||||
afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
支持聊天气泡样式和简洁样式切换
|
||||
支持导出对话为 Word 文档
|
||||
错误修复
|
||||
|
||||
@@ -1,14 +1,40 @@
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import { resolve } from 'path'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
||||
const visualizerPlugin = (type: 'renderer' | 'main') => {
|
||||
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
plugins: [
|
||||
externalizeDepsPlugin({
|
||||
exclude: [
|
||||
'@llm-tools/embedjs',
|
||||
'@llm-tools/embedjs-openai',
|
||||
'@llm-tools/embedjs-loader-web',
|
||||
'@llm-tools/embedjs-loader-markdown',
|
||||
'@llm-tools/embedjs-loader-msoffice',
|
||||
'@llm-tools/embedjs-loader-xml',
|
||||
'@llm-tools/embedjs-loader-pdf',
|
||||
'@llm-tools/embedjs-loader-sitemap',
|
||||
'@llm-tools/embedjs-libsql'
|
||||
]
|
||||
}),
|
||||
...visualizerPlugin('main')
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@main': resolve('src/main'),
|
||||
'@types': resolve('src/renderer/src/types'),
|
||||
'@main': resolve('src/main')
|
||||
'@shared': resolve('packages/shared')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['@libsql/client']
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -16,11 +42,15 @@ export default defineConfig({
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
renderer: {
|
||||
plugins: [react(), ...visualizerPlugin('renderer')],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@renderer': resolve('src/renderer/src')
|
||||
'@renderer': resolve('src/renderer/src'),
|
||||
'@shared': resolve('packages/shared')
|
||||
}
|
||||
},
|
||||
plugins: [react()]
|
||||
optimizeDeps: {
|
||||
exclude: ['chunk-RK3FTE5R.js']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
70
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "0.8.9",
|
||||
"version": "0.9.17",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -11,9 +11,11 @@
|
||||
"local",
|
||||
"packages/*"
|
||||
],
|
||||
"nohoist": [
|
||||
"packages/database"
|
||||
]
|
||||
"installConfig": {
|
||||
"hoistingLimits": [
|
||||
"packages/database"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
@@ -23,22 +25,45 @@
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build:check": "yarn typecheck",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||
"build:win": "dotenv npm run build && electron-builder --win --publish never",
|
||||
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
|
||||
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
|
||||
"build:win": "dotenv npm run build && electron-builder --win",
|
||||
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
||||
"build:mac": "dotenv electron-vite build && electron-builder --mac",
|
||||
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
|
||||
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
|
||||
"build:linux": "dotenv electron-vite build && electron-builder --linux",
|
||||
"build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64",
|
||||
"build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64",
|
||||
"build:npm": "node scripts/build-npm.js",
|
||||
"release": "node scripts/version.js",
|
||||
"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",
|
||||
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
||||
"analyze:main": "VISUALIZER_MAIN=true yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"@electron/notarize": "^2.5.0",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch",
|
||||
"@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch",
|
||||
"@llm-tools/embedjs-loader-csv": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch",
|
||||
"@llm-tools/embedjs-loader-msoffice": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-pdf": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-sitemap": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-web": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-xml": "^0.1.25",
|
||||
"@llm-tools/embedjs-openai": "^0.1.25",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"apache-arrow": "^18.1.0",
|
||||
"docx": "^9.0.2",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
@@ -48,7 +73,7 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"unzipper": "^0.12.3",
|
||||
"tokenx": "^0.4.1",
|
||||
"webdav": "4.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -56,31 +81,32 @@
|
||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@google/generative-ai": "^0.16.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@types/adm-zip": "^0",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
"@types/node": "^18.19.9",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/unzipper": "^0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.18.3",
|
||||
"antd": "^5.22.5",
|
||||
"applescript": "^1.0.0",
|
||||
"axios": "^1.7.3",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"electron": "^28.3.3",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron": "31.7.6",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"electron-vite": "^2.3.0",
|
||||
"emittery": "^1.0.3",
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"eslint": "^8.56.0",
|
||||
@@ -88,22 +114,21 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.0.0",
|
||||
"gpt-tokens": "^1.3.10",
|
||||
"i18next": "^23.11.5",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.4",
|
||||
"openai": "^4.52.1",
|
||||
"openai": "patch:openai@npm%3A4.76.2#~/.yarn/patches/openai-npm-4.76.2-8ff1374617.patch",
|
||||
"prettier": "^3.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router": "6",
|
||||
"react-router-dom": "6",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
@@ -111,7 +136,9 @@
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.77.2",
|
||||
"shiki": "^1.22.2",
|
||||
"styled-components": "^6.1.11",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^5.6.2",
|
||||
@@ -123,7 +150,8 @@
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
|
||||
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
||||
"@llm-tools/embedjs-utils@npm:0.1.25": "patch:@llm-tools/embedjs-utils@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-utils-npm-0.1.25-fd8fe8a193.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0"
|
||||
}
|
||||
|
||||
116
packages/shared/config/constant.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
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 代码文件
|
||||
'.cs' // C# 代码文件
|
||||
]
|
||||
|
||||
export const ZOOM_SHORTCUTS = [
|
||||
{
|
||||
key: 'zoom_in',
|
||||
shortcut: ['CommandOrControl', '='],
|
||||
editable: false,
|
||||
enabled: true,
|
||||
system: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_out',
|
||||
shortcut: ['CommandOrControl', '-'],
|
||||
editable: false,
|
||||
enabled: true,
|
||||
system: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_reset',
|
||||
shortcut: ['CommandOrControl', '0'],
|
||||
editable: false,
|
||||
enabled: true,
|
||||
system: true
|
||||
}
|
||||
]
|
||||
202
resources/cherry-studio/releases.html
Normal file
@@ -0,0 +1,202 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Github Releases Timeline</title>
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.10/dist/typography.min.css"></script>
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
|
||||
<div class="max-w-3xl mx-auto py-12 px-4">
|
||||
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
||||
|
||||
<!-- Loading状态 -->
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
||||
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error 状态 -->
|
||||
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
|
||||
|
||||
<!-- Release 列表 -->
|
||||
<div v-else class="space-y-8">
|
||||
<div v-for="release in releases" :key="release.id" class="relative pl-8"
|
||||
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
||||
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
||||
<div class="rounded-lg shadow-sm p-6 transition-shadow"
|
||||
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
|
||||
{{ release.name || release.tag_name }}
|
||||
</h2>
|
||||
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
||||
{{ formatDate(release.published_at) }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
||||
{{ release.tag_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
|
||||
v-html="renderMarkdown(release.body)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const md = window.markdownit({
|
||||
breaks: true,
|
||||
linkify: true
|
||||
})
|
||||
|
||||
const { createApp } = Vue
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
releases: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
isDark: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchReleases() {
|
||||
try {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch releases')
|
||||
}
|
||||
this.releases = await response.json()
|
||||
} catch (err) {
|
||||
this.error = 'Error loading releases: ' + err.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
},
|
||||
renderMarkdown(content) {
|
||||
if (!content) return ''
|
||||
return md.render(content)
|
||||
},
|
||||
initTheme() {
|
||||
// 从 URL 参数获取主题设置
|
||||
const url = new URL(window.location.href)
|
||||
const theme = url.searchParams.get('theme')
|
||||
this.isDark = theme === 'dark'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTheme()
|
||||
this.fetchReleases()
|
||||
}
|
||||
}).mount('#app')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 基础的 Markdown 样式 */
|
||||
.prose {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1.1em;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
list-style-type: decimal;
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 0.2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.dark .prose code {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
display: block;
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dark .prose a {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 4px solid #e5e7eb;
|
||||
padding-left: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.dark .prose blockquote {
|
||||
border-left-color: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dark .prose {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.dark-bg {
|
||||
background-color: #151515;
|
||||
}
|
||||
|
||||
.bg {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
117
resources/textMonitor.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class TextSelectionObserver: NSObject {
|
||||
let workspace = NSWorkspace.shared
|
||||
var lastSelectedText: String?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// 注册通知观察者
|
||||
let observer = NSWorkspace.shared.notificationCenter
|
||||
observer.addObserver(
|
||||
self,
|
||||
selector: #selector(handleSelectionChange),
|
||||
name: NSWorkspace.didActivateApplicationNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// 监听选择变化通知
|
||||
var axObserver: AXObserver?
|
||||
let error = AXObserverCreate(getpid(), { observer, element, notification, userData in
|
||||
let selfPointer = userData!.load(as: TextSelectionObserver.self)
|
||||
selfPointer.checkSelectedText()
|
||||
}, &axObserver)
|
||||
|
||||
if error == .success, let axObserver = axObserver {
|
||||
CFRunLoopAddSource(
|
||||
RunLoop.main.getCFRunLoop(),
|
||||
AXObserverGetRunLoopSource(axObserver),
|
||||
.defaultMode
|
||||
)
|
||||
|
||||
// 当前活动应用添加监听
|
||||
updateActiveAppObserver(axObserver)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleSelectionChange(_ notification: Notification) {
|
||||
// 应用切换时更新监听
|
||||
var axObserver: AXObserver?
|
||||
let error = AXObserverCreate(getpid(), { _, _, _, _ in }, &axObserver)
|
||||
if error == .success, let axObserver = axObserver {
|
||||
updateActiveAppObserver(axObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func updateActiveAppObserver(_ axObserver: AXObserver) {
|
||||
guard let app = workspace.frontmostApplication else { return }
|
||||
let pid = app.processIdentifier
|
||||
let element = AXUIElementCreateApplication(pid)
|
||||
|
||||
// 添加选择变化通知监听
|
||||
AXObserverAddNotification(
|
||||
axObserver,
|
||||
element,
|
||||
kAXSelectedTextChangedNotification as CFString,
|
||||
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
)
|
||||
}
|
||||
|
||||
func checkSelectedText() {
|
||||
if let text = getSelectedText() {
|
||||
if text.count > 0 && text != lastSelectedText {
|
||||
print(text)
|
||||
fflush(stdout)
|
||||
lastSelectedText = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSelectedText() -> String? {
|
||||
guard let app = NSWorkspace.shared.frontmostApplication else { return nil }
|
||||
let pid = app.processIdentifier
|
||||
|
||||
let axApp = AXUIElementCreateApplication(pid)
|
||||
var focusedElement: AnyObject?
|
||||
|
||||
// Get focused element
|
||||
let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement)
|
||||
guard result == .success else { return nil }
|
||||
|
||||
// Try different approaches to get selected text
|
||||
var selectedText: AnyObject?
|
||||
|
||||
// First try: Direct selected text
|
||||
var textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
|
||||
|
||||
// Second try: Selected text in text area
|
||||
if textResult != .success {
|
||||
var selectedTextRange: AnyObject?
|
||||
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextRangeAttribute as CFString, &selectedTextRange)
|
||||
if textResult == .success {
|
||||
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXValueAttribute as CFString, &selectedText)
|
||||
}
|
||||
}
|
||||
|
||||
// Third try: Get selected text from parent element
|
||||
if textResult != .success {
|
||||
var parent: AnyObject?
|
||||
if AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXParentAttribute as CFString, &parent) == .success {
|
||||
textResult = AXUIElementCopyAttributeValue(parent as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
|
||||
}
|
||||
}
|
||||
|
||||
guard textResult == .success, let text = selectedText as? String else { return nil }
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
let observer = TextSelectionObserver()
|
||||
|
||||
signal(SIGINT) { _ in
|
||||
exit(0)
|
||||
}
|
||||
|
||||
RunLoop.main.run()
|
||||
45
scripts/after-pack.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { Arch } = require('electron-builder')
|
||||
const { default: removeLocales } = require('./remove-locales')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
exports.default = async function (context) {
|
||||
await removeLocales(context)
|
||||
const platform = context.packager.platform.name
|
||||
const arch = context.arch
|
||||
|
||||
if (platform === 'mac') {
|
||||
const node_modules_path = path.join(
|
||||
context.appOutDir,
|
||||
'Cherry Studio.app',
|
||||
'Contents',
|
||||
'Resources',
|
||||
'app.asar.unpacked',
|
||||
'node_modules'
|
||||
)
|
||||
|
||||
removeDifferentArchNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64'])
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
|
||||
const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl']
|
||||
removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch)
|
||||
}
|
||||
|
||||
if (platform === 'windows') {
|
||||
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
|
||||
removeDifferentArchNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
|
||||
}
|
||||
}
|
||||
|
||||
function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) {
|
||||
const modulePath = path.join(nodeModulesPath, packageName)
|
||||
const dirs = fs.readdirSync(modulePath)
|
||||
dirs
|
||||
.filter((dir) => !arch.includes(dir))
|
||||
.forEach((dir) => {
|
||||
fs.rmSync(path.join(modulePath, dir), { recursive: true, force: true })
|
||||
console.log(`Removed dir: ${dir}`, arch)
|
||||
})
|
||||
}
|
||||
40
scripts/build-npm.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { downloadNpmPackage } = require('./utils')
|
||||
|
||||
async function downloadNpm(platform) {
|
||||
if (!platform || platform === 'mac') {
|
||||
downloadNpmPackage(
|
||||
'@libsql/darwin-arm64',
|
||||
'https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.4.7.tgz'
|
||||
)
|
||||
downloadNpmPackage('@libsql/darwin-x64', 'https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.4.7.tgz')
|
||||
}
|
||||
|
||||
if (!platform || platform === 'linux') {
|
||||
downloadNpmPackage(
|
||||
'@libsql/linux-arm64-gnu',
|
||||
'https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.7.tgz'
|
||||
)
|
||||
downloadNpmPackage(
|
||||
'@libsql/linux-arm64-musl',
|
||||
'https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.7.tgz'
|
||||
)
|
||||
downloadNpmPackage(
|
||||
'@libsql/linux-x64-gnu',
|
||||
'https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.7.tgz'
|
||||
)
|
||||
downloadNpmPackage(
|
||||
'@libsql/linux-x64-musl',
|
||||
'https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz'
|
||||
)
|
||||
}
|
||||
|
||||
if (!platform || platform === 'windows') {
|
||||
downloadNpmPackage(
|
||||
'@libsql/win32-x64-msvc',
|
||||
'https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const platformArg = process.argv[2]
|
||||
downloadNpm(platformArg)
|
||||
529
scripts/cloudflare-worker.js
Normal file
@@ -0,0 +1,529 @@
|
||||
// 配置信息
|
||||
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('开始定时检查新版本...')
|
||||
// 使用新的 checkNewRelease 函数
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新版本信息
|
||||
*/
|
||||
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 checkNewRelease(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: '请稍<E8AFB7><E7A88D><EFBFBD>再试'
|
||||
}),
|
||||
{
|
||||
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())
|
||||
}
|
||||
|
||||
// 移除版本检查,改为记录是否有文件更新的标志
|
||||
let hasUpdates = false
|
||||
if (versions.latestVersion !== version) {
|
||||
await addLog(env, 'INFO', `发现新版本: ${version}`)
|
||||
hasUpdates = true
|
||||
} else {
|
||||
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 || existingFile.size !== asset.size) {
|
||||
hasUpdates = true
|
||||
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', `文件${existingFile ? '更新' : '上传'}成功: ${asset.name}`)
|
||||
} else {
|
||||
// 文件存在且大小相同,标记为已上传
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在有更新或是新版本时才更新数据库和缓存
|
||||
if (hasUpdates) {
|
||||
// 更新版本记录
|
||||
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))
|
||||
await addLog(env, 'INFO', hasUpdates ? '更新完成' : '文件完整性检查完成')
|
||||
|
||||
// 清理旧版本
|
||||
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a))
|
||||
if (versionList.length > 2) {
|
||||
// 获取需要保留的两个最新版本
|
||||
const keepVersions = versionList.slice(0, 2)
|
||||
// 获取所有需要删除的版本
|
||||
const oldVersions = versionList.slice(2)
|
||||
|
||||
// 先获取 R2 桶中的所有文件列表
|
||||
const allFiles = await listAllFiles(env)
|
||||
|
||||
// 获取需要保留的文件名列表
|
||||
const keepFiles = new Set()
|
||||
for (const keepVersion of keepVersions) {
|
||||
const versionFiles = versions.versions[keepVersion].files
|
||||
versionFiles.forEach((file) => keepFiles.add(file.name))
|
||||
}
|
||||
|
||||
// 删除所有旧版本文件
|
||||
for (const oldVersion of oldVersions) {
|
||||
const oldFiles = versions.versions[oldVersion].files
|
||||
for (const file of oldFiles) {
|
||||
try {
|
||||
if (file.uploaded) {
|
||||
await env.R2_BUCKET.delete(file.name)
|
||||
await addLog(env, 'INFO', `删除旧文件: ${file.name}`)
|
||||
}
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', `删除旧文件失败: ${file.name}`, error.message)
|
||||
}
|
||||
}
|
||||
delete versions.versions[oldVersion]
|
||||
}
|
||||
|
||||
// 清理可能遗留的旧文件
|
||||
for (const file of allFiles) {
|
||||
if (!keepFiles.has(file.name)) {
|
||||
try {
|
||||
await env.R2_BUCKET.delete(file.name)
|
||||
await addLog(env, 'INFO', `删除遗留文件: ${file.name}`)
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', `删除遗留文件失败: ${file.name}`, error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存更新后的版本数据库
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2))
|
||||
}
|
||||
} else {
|
||||
await addLog(env, 'INFO', '所有文件完整性检查通过,无需更新')
|
||||
}
|
||||
|
||||
return hasUpdates ? cacheData : null
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', '检查新版本失败', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:获取 R2 桶中的所有文件列表
|
||||
async function listAllFiles(env) {
|
||||
const files = []
|
||||
let cursor
|
||||
|
||||
do {
|
||||
const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] })
|
||||
files.push(...listed.objects)
|
||||
cursor = listed.cursor
|
||||
} while (cursor)
|
||||
|
||||
return files
|
||||
}
|
||||
58
scripts/remove-locales.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
exports.default = async function (context) {
|
||||
const platform = context.packager.platform.name
|
||||
|
||||
// 根据平台确定 locales 目录位置
|
||||
let resourceDirs = []
|
||||
if (platform === 'mac') {
|
||||
// macOS 的语言文件位置
|
||||
resourceDirs = [
|
||||
path.join(context.appOutDir, 'Cherry Studio.app', 'Contents', 'Resources'),
|
||||
path.join(
|
||||
context.appOutDir,
|
||||
'Cherry Studio.app',
|
||||
'Contents',
|
||||
'Frameworks',
|
||||
'Electron Framework.framework',
|
||||
'Resources'
|
||||
)
|
||||
]
|
||||
} else {
|
||||
// Windows 和 Linux 的语言文件位置
|
||||
resourceDirs = [path.join(context.appOutDir, 'locales')]
|
||||
}
|
||||
|
||||
// 处理每个资源目录
|
||||
for (const resourceDir of resourceDirs) {
|
||||
if (!fs.existsSync(resourceDir)) {
|
||||
console.log(`Resource directory not found: ${resourceDir}, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取所有文件和目录
|
||||
const items = fs.readdirSync(resourceDir)
|
||||
|
||||
// 遍历并删除不需要的语言文件
|
||||
for (const item of items) {
|
||||
if (platform === 'mac') {
|
||||
// 在 macOS 上检查 .lproj 目录
|
||||
if (item.endsWith('.lproj') && !item.match(/^(en|zh|ru)/)) {
|
||||
const dirPath = path.join(resourceDir, item)
|
||||
fs.rmSync(dirPath, { recursive: true, force: true })
|
||||
console.log(`Removed locale directory: ${item} from ${resourceDir}`)
|
||||
}
|
||||
} else {
|
||||
// 其他平台处理 .pak 文件
|
||||
if (!item.match(/^(en|zh|ru)/)) {
|
||||
const filePath = path.join(resourceDir, item)
|
||||
fs.unlinkSync(filePath)
|
||||
console.log(`Removed locale file: ${item} from ${resourceDir}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Locale cleanup completed!')
|
||||
}
|
||||
58
scripts/replace-spaces.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// replaceSpaces.js
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const directory = 'dist'
|
||||
|
||||
// 处理文件名中的空格
|
||||
function replaceFileNames() {
|
||||
fs.readdir(directory, (err, files) => {
|
||||
if (err) throw err
|
||||
|
||||
files.forEach((file) => {
|
||||
const oldPath = path.join(directory, file)
|
||||
const newPath = path.join(directory, file.replace(/ /g, '-'))
|
||||
|
||||
fs.stat(oldPath, (err, stats) => {
|
||||
if (err) throw err
|
||||
|
||||
if (stats.isFile() && oldPath !== newPath) {
|
||||
fs.rename(oldPath, newPath, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`Renamed: ${oldPath} -> ${newPath}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function replaceYmlContent() {
|
||||
fs.readdir(directory, (err, files) => {
|
||||
if (err) throw err
|
||||
|
||||
files.forEach((file) => {
|
||||
if (path.extname(file).toLowerCase() === '.yml') {
|
||||
const filePath = path.join(directory, file)
|
||||
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) throw err
|
||||
|
||||
// 替换内容
|
||||
const newContent = data.replace(/Cherry Studio-/g, 'Cherry-Studio-')
|
||||
|
||||
// 写回文件
|
||||
fs.writeFile(filePath, newContent, 'utf8', (err) => {
|
||||
if (err) throw err
|
||||
console.log(`Updated content in: ${filePath}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 执行两个操作
|
||||
replaceFileNames()
|
||||
replaceYmlContent()
|
||||
@@ -1,26 +0,0 @@
|
||||
// replaceSpaces.js
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const directory = 'dist'
|
||||
|
||||
fs.readdir(directory, (err, files) => {
|
||||
if (err) throw err
|
||||
|
||||
files.forEach((file) => {
|
||||
const oldPath = path.join(directory, file)
|
||||
const newPath = path.join(directory, file.replace(/ /g, '-'))
|
||||
|
||||
fs.stat(oldPath, (err, stats) => {
|
||||
if (err) throw err
|
||||
|
||||
if (stats.isFile() && oldPath !== newPath) {
|
||||
fs.rename(oldPath, newPath, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`Renamed: ${oldPath} -> ${newPath}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
39
scripts/utils.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
function downloadNpmPackage(packageName, url) {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-download-'))
|
||||
|
||||
const targetDir = path.join('./node_modules/', packageName)
|
||||
const filename = packageName.replace('/', '-') + '.tgz'
|
||||
|
||||
// Skip if directory already exists
|
||||
if (fs.existsSync(targetDir)) {
|
||||
console.log(`${targetDir} already exists, skipping download...`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Downloading ${packageName}...`, url)
|
||||
const { execSync } = require('child_process')
|
||||
execSync(`curl --fail -o ${filename} ${url}`)
|
||||
|
||||
console.log(`Extracting ${filename}...`)
|
||||
execSync(`tar -xvf ${filename}`)
|
||||
execSync(`rm -rf ${filename}`)
|
||||
execSync(`mv package ${targetDir}`)
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${packageName}: ${error.message}`)
|
||||
if (fs.existsSync(filename)) {
|
||||
fs.unlinkSync(filename)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
downloadNpmPackage
|
||||
}
|
||||
@@ -1,91 +1,3 @@
|
||||
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 isMac = process.platform === 'darwin'
|
||||
export const isWin = process.platform === 'win32'
|
||||
export const isLinux = process.platform === 'linux'
|
||||
|
||||
9
src/main/electron.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare global {
|
||||
namespace Electron {
|
||||
interface App {
|
||||
isQuitting: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -3,67 +3,67 @@ import { app, BrowserWindow } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { registerZoomShortcut } from './services/ShortcutService'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
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
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
await updateUserDataPath()
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
await updateUserDataPath()
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
const mainWindow = windowService.createMainWindow()
|
||||
new TrayService()
|
||||
|
||||
app.on('activate', function () {
|
||||
// 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.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
windowService.createMainWindow()
|
||||
} else {
|
||||
windowService.showMainWindow()
|
||||
}
|
||||
})
|
||||
|
||||
registerShortcuts(mainWindow)
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for second instance
|
||||
app.on('second-instance', () => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0]
|
||||
if (mainWindow) {
|
||||
mainWindow.isMinimized() && mainWindow.restore()
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
// 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.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||
app.on('before-quit', () => {
|
||||
app.isQuitting = true
|
||||
})
|
||||
|
||||
const mainWindow = createMainWindow()
|
||||
|
||||
registerZoomShortcut(mainWindow)
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for second instance
|
||||
app.on('second-instance', () => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0]
|
||||
if (mainWindow) {
|
||||
mainWindow.isMinimized() && mainWindow.restore()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
}
|
||||
|
||||
128
src/main/ipc.ts
@@ -1,15 +1,24 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
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 BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileService from './services/FileService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { getResourcePath } from './utils'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import { createMinappWindow } from './window'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
@@ -22,25 +31,76 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged,
|
||||
appPath: app.getAppPath(),
|
||||
filesPath: path.join(app.getPath('userData'), 'Data', 'Files')
|
||||
filesPath: path.join(app.getPath('userData'), 'Data', 'Files'),
|
||||
appDataPath: app.getPath('userData'),
|
||||
resourcesPath: getResourcePath(),
|
||||
logsPath: log.transports.file.getFile().path
|
||||
}))
|
||||
|
||||
ipcMain.handle('app:proxy', (_, proxy: string) => session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {}))
|
||||
ipcMain.handle('app:proxy', async (_, proxy: string) => {
|
||||
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('app:reload', () => mainWindow.reload())
|
||||
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
|
||||
|
||||
// language
|
||||
ipcMain.handle('app:set-language', (_, language) => {
|
||||
configManager.setLanguage(language)
|
||||
})
|
||||
|
||||
// tray
|
||||
ipcMain.handle('app:set-tray', (_, isActive: boolean) => {
|
||||
configManager.setTray(isActive)
|
||||
})
|
||||
|
||||
ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray())
|
||||
|
||||
ipcMain.handle('config:set', (_, key: string, value: any) => {
|
||||
configManager.set(key, value)
|
||||
})
|
||||
|
||||
ipcMain.handle('config:get', (_, key: string) => {
|
||||
return configManager.get(key)
|
||||
})
|
||||
|
||||
// theme
|
||||
ipcMain.handle('app:set-theme', (_, theme: 'light' | 'dark') => {
|
||||
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,
|
||||
update: await autoUpdater.checkForUpdates()
|
||||
updateInfo: update?.updateInfo
|
||||
}
|
||||
})
|
||||
|
||||
@@ -56,6 +116,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// file
|
||||
ipcMain.handle('file:open', fileManager.open)
|
||||
ipcMain.handle('file:openPath', fileManager.openPath)
|
||||
ipcMain.handle('file:save', fileManager.save)
|
||||
ipcMain.handle('file:select', fileManager.selectFile)
|
||||
ipcMain.handle('file:upload', fileManager.uploadFile)
|
||||
@@ -70,10 +131,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('file:base64Image', fileManager.base64Image)
|
||||
ipcMain.handle('file:download', fileManager.downloadFile)
|
||||
ipcMain.handle('file:copy', fileManager.copyFile)
|
||||
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
|
||||
|
||||
// fs
|
||||
ipcMain.handle('fs:read', FileService.readFile)
|
||||
|
||||
// minapp
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
createMinappWindow({
|
||||
windowService.createMinappWindow({
|
||||
url: args.url,
|
||||
parent: mainWindow,
|
||||
windowOptions: {
|
||||
@@ -85,4 +150,53 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// export
|
||||
ipcMain.handle('export:word', exportService.exportToWord)
|
||||
|
||||
// open path
|
||||
ipcMain.handle('open:path', async (_, path: string) => {
|
||||
await shell.openPath(path)
|
||||
})
|
||||
|
||||
// shortcuts
|
||||
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
|
||||
configManager.setShortcuts(shortcuts)
|
||||
// Refresh shortcuts registration
|
||||
if (mainWindow) {
|
||||
unregisterAllShortcuts()
|
||||
registerShortcuts(mainWindow)
|
||||
}
|
||||
})
|
||||
|
||||
// knowledge base
|
||||
ipcMain.handle('knowledge-base:create', KnowledgeService.create)
|
||||
ipcMain.handle('knowledge-base:reset', KnowledgeService.reset)
|
||||
ipcMain.handle('knowledge-base:delete', KnowledgeService.delete)
|
||||
ipcMain.handle('knowledge-base:add', KnowledgeService.add)
|
||||
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
|
||||
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
|
||||
|
||||
// window
|
||||
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
|
||||
mainWindow?.setMinimumSize(width, height)
|
||||
})
|
||||
|
||||
ipcMain.handle('window:reset-minimum-size', () => {
|
||||
mainWindow?.setMinimumSize(1080, 600)
|
||||
const [width, height] = mainWindow?.getSize() ?? [1080, 600]
|
||||
if (width < 1080) {
|
||||
mainWindow?.setSize(1080, height)
|
||||
}
|
||||
})
|
||||
|
||||
// gemini
|
||||
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile)
|
||||
ipcMain.handle('gemini:base64-file', GeminiService.base64File)
|
||||
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile)
|
||||
ipcMain.handle('gemini:list-files', GeminiService.listFiles)
|
||||
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile)
|
||||
|
||||
// mini window
|
||||
ipcMain.handle('miniwindow:show', () => windowService.showMiniWindow())
|
||||
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
|
||||
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
|
||||
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
|
||||
}
|
||||
|
||||
BIN
src/main/resources/icon.ico
Normal file
|
After Width: | Height: | Size: 353 KiB |
@@ -1,15 +1,18 @@
|
||||
import { BrowserWindow, dialog } from 'electron'
|
||||
import { app, BrowserWindow, dialog } from 'electron'
|
||||
import logger from 'electron-log'
|
||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
||||
|
||||
import icon from '../../../build/icon.png?asset'
|
||||
|
||||
export default class AppUpdater {
|
||||
autoUpdater: _AppUpdater = autoUpdater
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
logger.transports.file.level = 'debug'
|
||||
logger.transports.file.level = 'info'
|
||||
|
||||
autoUpdater.logger = logger
|
||||
autoUpdater.forceDevUpdateConfig = true
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.forceDevUpdateConfig = !app.isPackaged
|
||||
autoUpdater.autoDownload = true
|
||||
|
||||
// 检测下载错误
|
||||
autoUpdater.on('error', (error) => {
|
||||
@@ -18,40 +21,8 @@ export default class AppUpdater {
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
||||
logger.info('检测到新版本', 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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 检测到不需要更新时
|
||||
@@ -61,23 +32,53 @@ export default class AppUpdater {
|
||||
|
||||
// 更新下载进度
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
logger.info('下载进度', progress)
|
||||
mainWindow.webContents.send('download-progress', progress)
|
||||
})
|
||||
|
||||
// 当需要更新的内容下载完成后
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
logger.info('下载完成,准备更新')
|
||||
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
||||
mainWindow.webContents.send('update-downloaded')
|
||||
|
||||
logger.info('下载完成,询问用户是否更新', releaseInfo)
|
||||
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: '安装更新',
|
||||
message: '更新下载完毕,应用将重启并进行安装'
|
||||
icon,
|
||||
message: `新版本 ${releaseInfo.version} 已准备就绪`,
|
||||
detail: this.formatReleaseNotes(releaseInfo.releaseNotes),
|
||||
buttons: ['稍后安装', '立即安装'],
|
||||
defaultId: 1,
|
||||
cancelId: 0
|
||||
})
|
||||
.then(() => {
|
||||
setImmediate(() => autoUpdater.quitAndInstall())
|
||||
.then(({ response }) => {
|
||||
if (response === 1) {
|
||||
app.isQuitting = true
|
||||
setImmediate(() => autoUpdater.quitAndInstall())
|
||||
} else {
|
||||
mainWindow.webContents.send('update-downloaded-cancelled')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.autoUpdater = autoUpdater
|
||||
}
|
||||
|
||||
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
||||
if (!releaseNotes) {
|
||||
return '暂无更新说明'
|
||||
}
|
||||
|
||||
if (typeof releaseNotes === 'string') {
|
||||
return releaseNotes
|
||||
}
|
||||
|
||||
return releaseNotes.map((note) => note.note).join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
interface ReleaseNoteInfo {
|
||||
readonly version: string
|
||||
readonly note: string | null
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import archiver from 'archiver'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import * as unzipper from 'unzipper'
|
||||
|
||||
import WebDav from './WebDav'
|
||||
|
||||
@@ -26,7 +25,6 @@ class BackupManager {
|
||||
destinationPath: string = this.backupDir
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
|
||||
// 将 data 写入临时文件
|
||||
@@ -38,21 +36,16 @@ class BackupManager {
|
||||
const tempDataDir = path.join(this.tempDir, 'Data')
|
||||
await fs.copy(sourcePath, tempDataDir)
|
||||
|
||||
// 创建 zip 文件
|
||||
const output = fs.createWriteStream(path.join(destinationPath, fileName))
|
||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
||||
|
||||
archive.pipe(output)
|
||||
archive.directory(this.tempDir, false)
|
||||
await archive.finalize()
|
||||
// 使用 adm-zip 创建压缩文件
|
||||
const zip = new AdmZip()
|
||||
zip.addLocalFolder(this.tempDir)
|
||||
const backupedFilePath = path.join(destinationPath, fileName)
|
||||
zip.writeZip(backupedFilePath)
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('Backup completed successfully')
|
||||
|
||||
const backupedFilePath = path.join(destinationPath, fileName)
|
||||
|
||||
return backupedFilePath
|
||||
} catch (error) {
|
||||
Logger.error('Backup failed:', error)
|
||||
@@ -61,31 +54,43 @@ class BackupManager {
|
||||
}
|
||||
|
||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
try {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
|
||||
// 解压备份文件到临时目录
|
||||
await fs
|
||||
.createReadStream(backupPath)
|
||||
.pipe(unzipper.Extract({ path: this.tempDir }))
|
||||
.promise()
|
||||
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
|
||||
|
||||
// 读取 data.json
|
||||
const dataPath = path.join(this.tempDir, 'data.json')
|
||||
const data = await fs.readFile(dataPath, 'utf-8')
|
||||
// 使用 adm-zip 解压
|
||||
const zip = new AdmZip(backupPath)
|
||||
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
|
||||
|
||||
// 恢复 Data 目录
|
||||
const sourcePath = path.join(this.tempDir, 'Data')
|
||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||
await fs.remove(destPath)
|
||||
await fs.copy(sourcePath, destPath)
|
||||
Logger.log('[backup] step 2: read data.json')
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
// 读取 data.json
|
||||
const dataPath = path.join(this.tempDir, 'data.json')
|
||||
const data = await fs.readFile(dataPath, 'utf-8')
|
||||
|
||||
Logger.log('Restore completed successfully')
|
||||
Logger.log('[backup] step 3: restore Data directory')
|
||||
|
||||
return data
|
||||
// 恢复 Data 目录
|
||||
const sourcePath = path.join(this.tempDir, 'Data')
|
||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||
await fs.remove(destPath)
|
||||
await fs.copy(sourcePath, destPath)
|
||||
|
||||
Logger.log('[backup] step 4: clean up temp directory')
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('[backup] step 5: Restore completed successfully')
|
||||
|
||||
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) {
|
||||
|
||||
74
src/main/services/CacheService.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
interface CacheItem<T> {
|
||||
data: T
|
||||
timestamp: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
export class CacheService {
|
||||
private static cache: Map<string, CacheItem<any>> = new Map()
|
||||
|
||||
/**
|
||||
* Set cache
|
||||
* @param key Cache key
|
||||
* @param data Cache data
|
||||
* @param duration Cache duration (in milliseconds)
|
||||
*/
|
||||
static set<T>(key: string, data: T, duration: number): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
duration
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache
|
||||
* @param key Cache key
|
||||
* @returns Returns data if cache exists and not expired, otherwise returns null
|
||||
*/
|
||||
static get<T>(key: string): T | null {
|
||||
const item = this.cache.get(key)
|
||||
if (!item) return null
|
||||
|
||||
const now = Date.now()
|
||||
if (now - item.timestamp > item.duration) {
|
||||
this.remove(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return item.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific cache
|
||||
* @param key Cache key
|
||||
*/
|
||||
static remove(key: string): void {
|
||||
this.cache.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
static clear(): void {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache exists and is valid
|
||||
* @param key Cache key
|
||||
* @returns boolean
|
||||
*/
|
||||
static has(key: string): boolean {
|
||||
const item = this.cache.get(key)
|
||||
if (!item) return false
|
||||
|
||||
const now = Date.now()
|
||||
if (now - item.timestamp > item.duration) {
|
||||
this.remove(key)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
118
src/main/services/ClipboardMonitor.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { debounce, getResourcePath } from '@main/utils'
|
||||
import { exec } from 'child_process'
|
||||
import { screen } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
export default class ClipboardMonitor {
|
||||
private platform: string
|
||||
private lastText: string
|
||||
private user32: any
|
||||
private observer: any
|
||||
public onTextSelected: (text: string) => void
|
||||
|
||||
constructor() {
|
||||
this.platform = process.platform
|
||||
this.lastText = ''
|
||||
this.onTextSelected = debounce((text: string) => this.handleTextSelected(text), 550)
|
||||
|
||||
if (this.platform === 'win32') {
|
||||
this.setupWindows()
|
||||
} else if (this.platform === 'darwin') {
|
||||
this.setupMacOS()
|
||||
}
|
||||
}
|
||||
|
||||
setupMacOS() {
|
||||
// 使用 Swift 脚本来监听文本选择
|
||||
const scriptPath = path.join(getResourcePath(), 'textMonitor.swift')
|
||||
|
||||
// 启动 Swift 进程来监听文本选择
|
||||
const process = exec(`swift ${scriptPath}`)
|
||||
|
||||
process?.stdout?.on('data', (data: string) => {
|
||||
console.log('[ClipboardMonitor] MacOS data:', data)
|
||||
const text = data.toString().trim()
|
||||
if (text && text !== this.lastText) {
|
||||
this.lastText = text
|
||||
this.onTextSelected(text)
|
||||
}
|
||||
})
|
||||
|
||||
process.on('error', (error) => {
|
||||
console.error('[ClipboardMonitor] MacOS error:', error)
|
||||
})
|
||||
}
|
||||
|
||||
setupWindows() {
|
||||
// 使用 Windows API 监听文本选择事件
|
||||
const ffi = require('ffi-napi')
|
||||
const ref = require('ref-napi')
|
||||
|
||||
this.user32 = new ffi.Library('user32', {
|
||||
SetWinEventHook: ['pointer', ['uint32', 'uint32', 'pointer', 'pointer', 'uint32', 'uint32', 'uint32']],
|
||||
UnhookWinEvent: ['bool', ['pointer']]
|
||||
})
|
||||
|
||||
// 定义事件常量
|
||||
const EVENT_OBJECT_SELECTION = 0x8006
|
||||
const WINEVENT_OUTOFCONTEXT = 0x0000
|
||||
const WINEVENT_SKIPOWNTHREAD = 0x0001
|
||||
const WINEVENT_SKIPOWNPROCESS = 0x0002
|
||||
|
||||
// 创建回调函数
|
||||
const callback = ffi.Callback('void', ['pointer', 'uint32', 'pointer', 'long', 'long', 'uint32', 'uint32'], () => {
|
||||
this.getSelectedText()
|
||||
})
|
||||
|
||||
// 设置事件钩子
|
||||
this.observer = this.user32.SetWinEventHook(
|
||||
EVENT_OBJECT_SELECTION,
|
||||
EVENT_OBJECT_SELECTION,
|
||||
ref.NULL,
|
||||
callback,
|
||||
0,
|
||||
0,
|
||||
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNTHREAD | WINEVENT_SKIPOWNPROCESS
|
||||
)
|
||||
}
|
||||
|
||||
getSelectedText() {
|
||||
// Get selected text
|
||||
if (this.platform === 'win32') {
|
||||
const ref = require('ref-napi')
|
||||
if (this.user32.OpenClipboard(ref.NULL)) {
|
||||
// Get clipboard content
|
||||
const text = this.user32.GetClipboardData(1) // CF_TEXT = 1
|
||||
this.user32.CloseClipboard()
|
||||
|
||||
if (text && text !== this.lastText) {
|
||||
this.lastText = text
|
||||
this.onTextSelected(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleTextSelected(text: string) {
|
||||
if (!text) return
|
||||
|
||||
console.debug('[ClipboardMonitor] handleTextSelected', text)
|
||||
|
||||
windowService.setLastSelectedText(text)
|
||||
|
||||
const mousePosition = screen.getCursorScreenPoint()
|
||||
|
||||
windowService.showSelectionMenu({
|
||||
x: mousePosition.x,
|
||||
y: mousePosition.y + 10
|
||||
})
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.platform === 'win32' && this.observer) {
|
||||
this.user32.UnhookWinEvent(this.observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,112 @@
|
||||
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()
|
||||
}
|
||||
|
||||
getTheme(): 'light' | 'dark' {
|
||||
return this.store.get('theme', 'light') as 'light' | 'dark'
|
||||
getLanguage(): LanguageVarious {
|
||||
const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : 'en-US'
|
||||
return this.store.get('language', locale) as LanguageVarious
|
||||
}
|
||||
|
||||
setTheme(theme: 'light' | 'dark') {
|
||||
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)
|
||||
}
|
||||
|
||||
getTray(): 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.filter((shortcut) => shortcut.system)
|
||||
)
|
||||
this.notifySubscribers('shortcuts', shortcuts)
|
||||
}
|
||||
|
||||
getClickTrayToShowQuickAssistant(): boolean {
|
||||
return this.store.get('clickTrayToShowQuickAssistant', false) as boolean
|
||||
}
|
||||
|
||||
setClickTrayToShowQuickAssistant(value: boolean) {
|
||||
this.store.set('clickTrayToShowQuickAssistant', value)
|
||||
}
|
||||
|
||||
getEnableQuickAssistant(): boolean {
|
||||
return this.store.get('enableQuickAssistant', false) as boolean
|
||||
}
|
||||
|
||||
setEnableQuickAssistant(value: boolean) {
|
||||
this.store.set('enableQuickAssistant', value)
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
this.store.set(key, value)
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return this.store.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
export const configManager = new ConfigManager()
|
||||
|
||||
7
src/main/services/FileService.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
export default class FileService {
|
||||
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
|
||||
return fs.readFileSync(path, 'utf8')
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { documentExts, imageExts } from '@main/constant'
|
||||
import { getFileType } from '@main/utils/file'
|
||||
import { documentExts, imageExts } from '@shared/config/constant'
|
||||
import { FileType } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import {
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
OpenDialogOptions,
|
||||
OpenDialogReturnValue,
|
||||
SaveDialogOptions,
|
||||
SaveDialogReturnValue
|
||||
SaveDialogReturnValue,
|
||||
shell
|
||||
} from 'electron'
|
||||
import logger from 'electron-log'
|
||||
import * as fs from 'fs'
|
||||
@@ -262,11 +263,23 @@ class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public binaryFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
|
||||
const filePath = path.join(this.storageDir, id)
|
||||
const data = await fs.promises.readFile(filePath)
|
||||
const mime = `image/${path.extname(filePath).slice(1)}`
|
||||
return { data, mime }
|
||||
}
|
||||
|
||||
public clear = async (): Promise<void> => {
|
||||
await fs.promises.rmdir(this.storageDir, { recursive: true })
|
||||
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 (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
options: OpenDialogOptions
|
||||
@@ -293,6 +306,10 @@ class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public openPath = async (_: Electron.IpcMainInvokeEvent, path: string): Promise<void> => {
|
||||
shell.openPath(path).catch((err) => logger.error('[IPC - Error] Failed to open file:', err))
|
||||
}
|
||||
|
||||
public save = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
fileName: string,
|
||||
@@ -371,7 +388,7 @@ class FileStorage {
|
||||
}
|
||||
|
||||
// 如果URL中有文件名,使用URL中的文件名
|
||||
const urlFilename = url.split('/').pop()
|
||||
const urlFilename = url.split('/').pop()?.split('?')[0]
|
||||
if (urlFilename && urlFilename.includes('.')) {
|
||||
filename = urlFilename
|
||||
}
|
||||
|
||||
63
src/main/services/GeminiService.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { FileMetadataResponse, FileState, GoogleAIFileManager } from '@google/generative-ai/server'
|
||||
import { FileType } from '@types'
|
||||
import fs from 'fs'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
|
||||
export class GeminiService {
|
||||
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
|
||||
private static readonly CACHE_DURATION = 3000
|
||||
|
||||
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
const uploadResult = await fileManager.uploadFile(file.path, {
|
||||
mimeType: 'application/pdf',
|
||||
displayName: file.origin_name
|
||||
})
|
||||
return uploadResult
|
||||
}
|
||||
|
||||
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
|
||||
return {
|
||||
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
|
||||
mimeType: 'application/pdf'
|
||||
}
|
||||
}
|
||||
|
||||
static async retrieveFile(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
file: FileType,
|
||||
apiKey: string
|
||||
): Promise<FileMetadataResponse | undefined> {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
|
||||
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
|
||||
if (cachedResponse) {
|
||||
return GeminiService.processResponse(cachedResponse, file)
|
||||
}
|
||||
|
||||
const response = await fileManager.listFiles()
|
||||
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
|
||||
|
||||
return GeminiService.processResponse(response, file)
|
||||
}
|
||||
|
||||
private static processResponse(response: any, file: FileType) {
|
||||
if (response.files) {
|
||||
return response.files
|
||||
.filter((file) => file.state === FileState.ACTIVE)
|
||||
.find((i) => i.displayName === file.origin_name && Number(i.sizeBytes) === file.size)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
return await fileManager.listFiles()
|
||||
}
|
||||
|
||||
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
await fileManager.deleteFile(fileId)
|
||||
}
|
||||
}
|
||||
205
src/main/services/KnowledgeService.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import * as fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { LocalPathLoader, RAGApplication, RAGApplicationBuilder, TextLoader } from '@llm-tools/embedjs'
|
||||
import type { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { LibSqlDb } from '@llm-tools/embedjs-libsql'
|
||||
import { MarkdownLoader } from '@llm-tools/embedjs-loader-markdown'
|
||||
import { DocxLoader, ExcelLoader, PptLoader } from '@llm-tools/embedjs-loader-msoffice'
|
||||
import { PdfLoader } from '@llm-tools/embedjs-loader-pdf'
|
||||
import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
|
||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
||||
import { getInstanceName } from '@main/utils'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
|
||||
import { app } from 'electron'
|
||||
|
||||
class KnowledgeService {
|
||||
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
|
||||
|
||||
constructor() {
|
||||
this.initStorageDir()
|
||||
}
|
||||
|
||||
private initStorageDir = (): void => {
|
||||
if (!fs.existsSync(this.storageDir)) {
|
||||
fs.mkdirSync(this.storageDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
private getRagApplication = async ({
|
||||
id,
|
||||
model,
|
||||
apiKey,
|
||||
apiVersion,
|
||||
baseURL,
|
||||
dimensions
|
||||
}: KnowledgeBaseParams): Promise<RAGApplication> => {
|
||||
return new RAGApplicationBuilder()
|
||||
.setModel('NO_MODEL')
|
||||
.setEmbeddingModel(
|
||||
apiVersion
|
||||
? new AzureOpenAiEmbeddings({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
||||
dimensions,
|
||||
batchSize: 10
|
||||
})
|
||||
: new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
configuration: { baseURL },
|
||||
dimensions,
|
||||
batchSize: 10
|
||||
})
|
||||
)
|
||||
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
|
||||
.build()
|
||||
}
|
||||
|
||||
public create = async (_: Electron.IpcMainInvokeEvent, base: KnowledgeBaseParams): Promise<void> => {
|
||||
this.getRagApplication(base)
|
||||
}
|
||||
|
||||
public reset = async (_: Electron.IpcMainInvokeEvent, { base }: { base: KnowledgeBaseParams }): Promise<void> => {
|
||||
const ragApplication = await this.getRagApplication(base)
|
||||
await ragApplication.reset()
|
||||
}
|
||||
|
||||
public delete = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
|
||||
const dbPath = path.join(this.storageDir, id)
|
||||
if (fs.existsSync(dbPath)) {
|
||||
fs.rmSync(dbPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
public add = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ base, item, forceReload = false }: { base: KnowledgeBaseParams; item: KnowledgeItem; forceReload: boolean }
|
||||
): Promise<AddLoaderReturn> => {
|
||||
const ragApplication = await this.getRagApplication(base)
|
||||
|
||||
if (item.type === 'directory') {
|
||||
const directory = item.content as string
|
||||
return await ragApplication.addLoader(
|
||||
new LocalPathLoader({ path: directory, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'url') {
|
||||
const content = item.content as string
|
||||
if (content.startsWith('http')) {
|
||||
return await ragApplication.addLoader(
|
||||
new WebLoader({ urlOrContent: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.type === 'sitemap') {
|
||||
const content = item.content as string
|
||||
// @ts-ignore loader type
|
||||
return await ragApplication.addLoader(
|
||||
new SitemapLoader({ url: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'note') {
|
||||
const content = item.content as string
|
||||
return await ragApplication.addLoader(
|
||||
new TextLoader({ text: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
const file = item.content as FileType
|
||||
|
||||
if (file.ext === '.pdf') {
|
||||
return await ragApplication.addLoader(
|
||||
new PdfLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (file.ext === '.docx') {
|
||||
return await ragApplication.addLoader(
|
||||
new DocxLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (file.ext === '.pptx') {
|
||||
return await ragApplication.addLoader(
|
||||
new PptLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (file.ext === '.xlsx') {
|
||||
return await ragApplication.addLoader(
|
||||
new ExcelLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (['.md'].includes(file.ext)) {
|
||||
return await ragApplication.addLoader(
|
||||
new MarkdownLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(file.path, 'utf-8')
|
||||
|
||||
return await ragApplication.addLoader(
|
||||
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
return { entriesAdded: 0, uniqueId: '', loaderType: '' }
|
||||
}
|
||||
|
||||
public remove = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }
|
||||
): Promise<void> => {
|
||||
const ragApplication = await this.getRagApplication(base)
|
||||
await ragApplication.deleteLoader(uniqueId)
|
||||
}
|
||||
|
||||
public search = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ search, base }: { search: string; base: KnowledgeBaseParams }
|
||||
): Promise<ExtractChunkData[]> => {
|
||||
const ragApplication = await this.getRagApplication(base)
|
||||
return await ragApplication.search(search)
|
||||
}
|
||||
}
|
||||
|
||||
export default new KnowledgeService()
|
||||
@@ -1,53 +1,147 @@
|
||||
import { Shortcut } from '@types'
|
||||
import { BrowserWindow, globalShortcut } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
export function registerZoomShortcut(mainWindow: BrowserWindow) {
|
||||
const registerShortcuts = () => {
|
||||
// 注册放大快捷键 (Ctrl+Plus 或 Cmd+Plus)
|
||||
globalShortcut.register('CommandOrControl+=', () => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom + 0.1
|
||||
// Prevent zoom factor from exceeding reasonable limits
|
||||
if (newZoom <= 5.0) {
|
||||
mainWindow.webContents.setZoomFactor(newZoom)
|
||||
import { configManager } from './ConfigManager'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
let showAppAccelerator: string | null = null
|
||||
let showMiniWindowAccelerator: 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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 注册缩小快捷键 (Ctrl+Minus 或 Cmd+Minus)
|
||||
globalShortcut.register('CommandOrControl+-', () => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom - 0.1
|
||||
// Prevent zoom factor from going below 0.1
|
||||
if (newZoom >= 0.1) {
|
||||
mainWindow.webContents.setZoomFactor(newZoom)
|
||||
}
|
||||
case 'mini_window':
|
||||
return () => {
|
||||
windowService.toggleMiniWindow()
|
||||
}
|
||||
})
|
||||
|
||||
// 注册重置缩放快捷键 (Ctrl+0 或 Cmd+0)
|
||||
globalShortcut.register('CommandOrControl+0', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.setZoomFactor(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unregisterShortcuts = () => {
|
||||
globalShortcut.unregister('CommandOrControl+=')
|
||||
globalShortcut.unregister('CommandOrControl+-')
|
||||
globalShortcut.unregister('CommandOrControl+0')
|
||||
}
|
||||
|
||||
// 当窗口获得焦点时注册快捷键
|
||||
mainWindow.on('focus', registerShortcuts)
|
||||
|
||||
// 当窗口失去焦点时注销快捷键
|
||||
mainWindow.on('blur', unregisterShortcuts)
|
||||
|
||||
// 初始注册(如果窗口已经处于焦点状态)
|
||||
if (mainWindow.isFocused()) {
|
||||
registerShortcuts()
|
||||
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) => {
|
||||
try {
|
||||
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 === 'mini_window') {
|
||||
showMiniWindowAccelerator = 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(formatShortcutKey(shortcut.shortcut), () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unregister = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
try {
|
||||
globalShortcut.unregisterAll()
|
||||
|
||||
if (showAppAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
|
||||
}
|
||||
|
||||
if (showMiniWindowAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
||||
handler && globalShortcut.register(showMiniWindowAccelerator, () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[ShortcutService] Failed to unregister shortcuts')
|
||||
}
|
||||
}
|
||||
|
||||
window.on('focus', () => register())
|
||||
window.on('blur', () => unregister())
|
||||
|
||||
if (!window.isDestroyed() && window.isFocused()) {
|
||||
register()
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterAllShortcuts() {
|
||||
try {
|
||||
showAppAccelerator = null
|
||||
showMiniWindowAccelerator = null
|
||||
globalShortcut.unregisterAll()
|
||||
} catch (error) {
|
||||
Logger.error('[ShortcutService] Failed to unregister all shortcuts')
|
||||
}
|
||||
}
|
||||
|
||||
118
src/main/services/TrayService.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { isMac } from '@main/constant'
|
||||
import { locales } from '@main/utils/locales'
|
||||
import { app, Menu, MenuItemConstructorOptions, 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 static instance: TrayService
|
||||
private tray: Tray | null = null
|
||||
|
||||
constructor() {
|
||||
this.updateTray()
|
||||
this.watchTrayChanges()
|
||||
TrayService.instance = this
|
||||
}
|
||||
|
||||
public static getInstance() {
|
||||
return TrayService.instance
|
||||
}
|
||||
|
||||
private createTray() {
|
||||
this.destroyTray()
|
||||
|
||||
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 enableQuickAssistant = configManager.getEnableQuickAssistant()
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: trayLocale.show_window,
|
||||
click: () => windowService.showMainWindow()
|
||||
},
|
||||
enableQuickAssistant && {
|
||||
label: trayLocale.show_mini_window,
|
||||
click: () => windowService.showMiniWindow()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: trayLocale.quit,
|
||||
click: () => this.quit()
|
||||
}
|
||||
].filter(Boolean) as MenuItemConstructorOptions[]
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate(template)
|
||||
|
||||
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', () => {
|
||||
if (enableQuickAssistant && configManager.getClickTrayToShowQuickAssistant()) {
|
||||
windowService.showMiniWindow()
|
||||
} else {
|
||||
windowService.showMainWindow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private updateTray() {
|
||||
const showTray = configManager.getTray()
|
||||
if (showTray) {
|
||||
this.createTray()
|
||||
} else {
|
||||
this.destroyTray()
|
||||
}
|
||||
}
|
||||
|
||||
public restartTray() {
|
||||
if (configManager.getTray()) {
|
||||
this.destroyTray()
|
||||
this.createTray()
|
||||
}
|
||||
}
|
||||
|
||||
private destroyTray() {
|
||||
if (this.tray) {
|
||||
this.tray.destroy()
|
||||
this.tray = null
|
||||
}
|
||||
}
|
||||
|
||||
private watchTrayChanges() {
|
||||
configManager.subscribe<boolean>('tray', () => this.updateTray())
|
||||
}
|
||||
|
||||
private quit() {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
394
src/main/services/WindowService.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { isLinux, isWin } from '@main/constant'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import path, { 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
|
||||
private miniWindow: BrowserWindow | null = null
|
||||
private wasFullScreen: boolean = false
|
||||
private selectionMenuWindow: BrowserWindow | null = null
|
||||
private lastSelectedText: string = ''
|
||||
|
||||
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'
|
||||
const isLinux = process.platform === 'linux'
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: false, // 初始不显示
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'under-window',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: isLinux ? 'default' : '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.once('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
// 处理全屏相关事件
|
||||
mainWindow.on('enter-full-screen', () => {
|
||||
this.wasFullScreen = true
|
||||
mainWindow.webContents.send('fullscreen-status-changed', true)
|
||||
})
|
||||
|
||||
mainWindow.on('leave-full-screen', () => {
|
||||
this.wasFullScreen = false
|
||||
mainWindow.webContents.send('fullscreen-status-changed', false)
|
||||
})
|
||||
}
|
||||
|
||||
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
if (url.includes('localhost:5173')) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
const { url } = details
|
||||
|
||||
if (url.includes('http://file/')) {
|
||||
const fileName = url.replace('http://file/', '')
|
||||
const storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
|
||||
const filePath = storageDir + '/' + fileName
|
||||
shell.openPath(filePath).catch((err) => Logger.error('Failed to open file:', err))
|
||||
} else {
|
||||
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) => {
|
||||
// 如果已经触发退出,直接退出
|
||||
if (app.isQuitting) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// 没有开启托盘,且是Windows或Linux系统,直接退出
|
||||
const notInTray = !configManager.getTray()
|
||||
if ((isWin || isLinux) && notInTray) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// 如果是全屏状态,直接退出
|
||||
if (this.wasFullScreen) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
public showMiniWindow() {
|
||||
const enableQuickAssistant = configManager.getEnableQuickAssistant()
|
||||
|
||||
if (!enableQuickAssistant) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectionMenuWindow) {
|
||||
this.selectionMenuWindow.hide()
|
||||
}
|
||||
|
||||
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||
if (this.miniWindow.isMinimized()) {
|
||||
this.miniWindow.restore()
|
||||
}
|
||||
this.miniWindow.show()
|
||||
this.miniWindow.center()
|
||||
this.miniWindow.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.miniWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 520,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'under-window',
|
||||
visualEffectState: 'followWindow',
|
||||
center: true,
|
||||
frame: false,
|
||||
alwaysOnTop: true,
|
||||
resizable: false,
|
||||
useContentSize: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
}
|
||||
})
|
||||
|
||||
this.miniWindow.on('blur', () => {
|
||||
this.miniWindow?.hide()
|
||||
})
|
||||
|
||||
this.miniWindow.on('closed', () => {
|
||||
this.miniWindow = null
|
||||
})
|
||||
|
||||
this.miniWindow.on('hide', () => {
|
||||
this.miniWindow?.webContents.send('hide-mini-window')
|
||||
})
|
||||
|
||||
this.miniWindow.on('show', () => {
|
||||
this.miniWindow?.webContents.send('show-mini-window')
|
||||
})
|
||||
|
||||
ipcMain.on('miniwindow-reload', () => {
|
||||
this.miniWindow?.reload()
|
||||
})
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/mini')
|
||||
} else {
|
||||
this.miniWindow.loadFile(join(__dirname, '../renderer/index.html'), {
|
||||
hash: '#/mini'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public hideMiniWindow() {
|
||||
this.miniWindow?.hide()
|
||||
}
|
||||
|
||||
public closeMiniWindow() {
|
||||
this.miniWindow?.close()
|
||||
}
|
||||
|
||||
public toggleMiniWindow() {
|
||||
if (this.miniWindow) {
|
||||
this.miniWindow.isVisible() ? this.miniWindow.hide() : this.miniWindow.show()
|
||||
} else {
|
||||
this.showMiniWindow()
|
||||
}
|
||||
}
|
||||
|
||||
public showSelectionMenu(bounds: { x: number; y: number }) {
|
||||
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||
this.selectionMenuWindow.setPosition(bounds.x, bounds.y)
|
||||
this.selectionMenuWindow.show()
|
||||
return
|
||||
}
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.selectionMenuWindow = new BrowserWindow({
|
||||
width: 280,
|
||||
height: 40,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
alwaysOnTop: false,
|
||||
skipTaskbar: true,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
resizable: false,
|
||||
vibrancy: 'popover',
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false
|
||||
}
|
||||
})
|
||||
|
||||
// 点击其他地方时隐藏窗口
|
||||
this.selectionMenuWindow.on('blur', () => {
|
||||
this.selectionMenuWindow?.hide()
|
||||
this.miniWindow?.webContents.send('selection-action', {
|
||||
action: 'home',
|
||||
selectedText: this.lastSelectedText
|
||||
})
|
||||
})
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
this.selectionMenuWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/menu/menu.html')
|
||||
} else {
|
||||
this.selectionMenuWindow.loadFile(join(__dirname, '../renderer/src/windows/menu/menu.html'))
|
||||
}
|
||||
|
||||
this.setupSelectionMenuEvents()
|
||||
}
|
||||
|
||||
private setupSelectionMenuEvents() {
|
||||
if (!this.selectionMenuWindow) return
|
||||
|
||||
ipcMain.removeHandler('selection-menu:action')
|
||||
ipcMain.handle('selection-menu:action', (_, action) => {
|
||||
this.selectionMenuWindow?.hide()
|
||||
this.showMiniWindow()
|
||||
setTimeout(() => {
|
||||
this.miniWindow?.webContents.send('selection-action', {
|
||||
action,
|
||||
selectedText: this.lastSelectedText
|
||||
})
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
public setLastSelectedText(text: string) {
|
||||
this.lastSelectedText = text
|
||||
}
|
||||
}
|
||||
|
||||
export const windowService = WindowService.getInstance()
|
||||
@@ -1,6 +1,5 @@
|
||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@main/constant'
|
||||
|
||||
import { FileTypes } from '../../renderer/src/types'
|
||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
|
||||
import { FileTypes } from '@types'
|
||||
|
||||
export function getFileType(ext: string): FileTypes {
|
||||
ext = ext.toLowerCase()
|
||||
|
||||
@@ -14,3 +14,31 @@ export function getDataPath() {
|
||||
}
|
||||
return dataPath
|
||||
}
|
||||
|
||||
export function getInstanceName(baseURL: string) {
|
||||
try {
|
||||
return new URL(baseURL).host.split('.')[0]
|
||||
} catch (error) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function debounce(func: (...args: any[]) => void, wait: number, immediate: boolean = false) {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
return function (...args: any[]) {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
if (immediate) {
|
||||
func(...args)
|
||||
} else {
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dumpPersistState() {
|
||||
const persistState = JSON.parse(localStorage.getItem('persist:cherry-studio') || '{}')
|
||||
for (const key in persistState) {
|
||||
persistState[key] = JSON.parse(persistState[key])
|
||||
}
|
||||
return JSON.stringify(persistState)
|
||||
}
|
||||
|
||||
15
src/main/utils/locales.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import EnUs from '../../renderer/src/i18n/locales/en-us.json'
|
||||
import JaJP from '../../renderer/src/i18n/locales/ja-jp.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,
|
||||
'ja-JP': JaJP,
|
||||
'ru-RU': RuRu
|
||||
}
|
||||
|
||||
export { locales }
|
||||
16
src/main/utils/windowUtil.ts
Normal 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 }
|
||||
@@ -1,128 +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 { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
|
||||
export function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
|
||||
// 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
|
||||
}
|
||||
66
src/preload/index.d.ts
vendored
@@ -1,25 +1,28 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type { FileMetadataResponse, ListFilesResponse, UploadFileResponse } from '@google/generative-ai/server'
|
||||
import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { FileType } from '@renderer/types'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
import { AppInfo, KnowledgeBaseParams, KnowledgeItem, LanguageVarious } from '@renderer/types'
|
||||
import type { OpenDialogOptions } from 'electron'
|
||||
import type { UpdateInfo } from 'electron-updater'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: {
|
||||
getAppInfo: () => Promise<{
|
||||
version: string
|
||||
isPackaged: boolean
|
||||
appPath: string
|
||||
filesPath: string
|
||||
}>
|
||||
checkForUpdate: () => void
|
||||
getAppInfo: () => Promise<AppInfo>
|
||||
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
setLanguage: (theme: LanguageVarious) => void
|
||||
setTray: (isActive: boolean) => void
|
||||
restartTray: () => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||
reload: () => void
|
||||
clearCache: () => Promise<{ success: boolean; error?: string }>
|
||||
zip: {
|
||||
compress: (text: string) => Promise<Buffer>
|
||||
decompress: (text: Buffer) => Promise<string>
|
||||
@@ -41,6 +44,7 @@ declare global {
|
||||
create: (fileName: string) => Promise<string>
|
||||
write: (filePath: string, data: Uint8Array | string) => Promise<void>
|
||||
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
|
||||
openPath: (path: string) => Promise<void>
|
||||
save: (
|
||||
path: string,
|
||||
content: string | NodeJS.ArrayBufferView,
|
||||
@@ -50,10 +54,58 @@ declare global {
|
||||
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||
download: (url: string) => Promise<FileType | null>
|
||||
copy: (fileId: string, destPath: string) => Promise<void>
|
||||
binaryFile: (fileId: string) => Promise<{ data: Buffer; mime: string }>
|
||||
}
|
||||
fs: {
|
||||
read: (path: string) => Promise<string>
|
||||
}
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => Promise<void>
|
||||
}
|
||||
openPath: (path: string) => Promise<void>
|
||||
shortcuts: {
|
||||
update: (shortcuts: Shortcut[]) => Promise<void>
|
||||
}
|
||||
knowledgeBase: {
|
||||
create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) => Promise<void>
|
||||
reset: ({ base }: { base: KnowledgeBaseParams }) => Promise<void>
|
||||
delete: (id: string) => Promise<void>
|
||||
add: ({
|
||||
base,
|
||||
item,
|
||||
forceReload = false
|
||||
}: {
|
||||
base: KnowledgeBaseParams
|
||||
item: KnowledgeItem
|
||||
forceReload?: boolean
|
||||
}) => Promise<AddLoaderReturn>
|
||||
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
|
||||
}
|
||||
window: {
|
||||
setMinimumSize: (width: number, height: number) => Promise<void>
|
||||
resetMinimumSize: () => Promise<void>
|
||||
}
|
||||
gemini: {
|
||||
uploadFile: (file: FileType, apiKey: string) => Promise<UploadFileResponse>
|
||||
retrieveFile: (file: FileType, apiKey: string) => Promise<FileMetadataResponse | undefined>
|
||||
base64File: (file: FileType) => Promise<{ data: string; mimeType: string }>
|
||||
listFiles: (apiKey: string) => Promise<ListFilesResponse>
|
||||
deleteFile: (apiKey: string, fileId: string) => Promise<void>
|
||||
}
|
||||
selectionMenu: {
|
||||
action: (action: string) => Promise<void>
|
||||
}
|
||||
config: {
|
||||
set: (key: string, value: any) => Promise<void>
|
||||
get: (key: string) => Promise<any>
|
||||
}
|
||||
miniWindow: {
|
||||
show: () => Promise<void>
|
||||
hide: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
toggle: () => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { WebDavConfig } from '@types'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@@ -8,9 +8,13 @@ const api = {
|
||||
reload: () => ipcRenderer.invoke('app:reload'),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy),
|
||||
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
|
||||
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
|
||||
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
|
||||
restartTray: () => ipcRenderer.invoke('app:restart-tray'),
|
||||
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),
|
||||
clearCache: () => ipcRenderer.invoke('app:clear-cache'),
|
||||
zip: {
|
||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
||||
@@ -33,16 +37,68 @@ const api = {
|
||||
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
|
||||
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data),
|
||||
open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options),
|
||||
openPath: (path: string) => ipcRenderer.invoke('file:openPath', path),
|
||||
save: (path: string, content: string, options?: { compress: boolean }) =>
|
||||
ipcRenderer.invoke('file:save', path, content, options),
|
||||
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
|
||||
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)
|
||||
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath),
|
||||
binaryFile: (fileId: string) => ipcRenderer.invoke('file:binaryFile', fileId)
|
||||
},
|
||||
fs: {
|
||||
read: (path: string) => ipcRenderer.invoke('fs:read', path)
|
||||
},
|
||||
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)
|
||||
},
|
||||
knowledgeBase: {
|
||||
create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) =>
|
||||
ipcRenderer.invoke('knowledge-base:create', { id, model, apiKey, baseURL }),
|
||||
reset: ({ base }: { base: KnowledgeBaseParams }) => ipcRenderer.invoke('knowledge-base:reset', { base }),
|
||||
delete: (id: string) => ipcRenderer.invoke('knowledge-base:delete', id),
|
||||
add: ({
|
||||
base,
|
||||
item,
|
||||
forceReload = false
|
||||
}: {
|
||||
base: KnowledgeBaseParams
|
||||
item: KnowledgeItem
|
||||
forceReload?: boolean
|
||||
}) => ipcRenderer.invoke('knowledge-base:add', { base, item, forceReload }),
|
||||
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) =>
|
||||
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }),
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
|
||||
ipcRenderer.invoke('knowledge-base:search', { search, base })
|
||||
},
|
||||
window: {
|
||||
setMinimumSize: (width: number, height: number) => ipcRenderer.invoke('window:set-minimum-size', width, height),
|
||||
resetMinimumSize: () => ipcRenderer.invoke('window:reset-minimum-size')
|
||||
},
|
||||
gemini: {
|
||||
uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:upload-file', file, apiKey),
|
||||
base64File: (file: FileType) => ipcRenderer.invoke('gemini:base64-file', file),
|
||||
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey),
|
||||
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey),
|
||||
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId)
|
||||
},
|
||||
selectionMenu: {
|
||||
action: (action: string) => ipcRenderer.invoke('selection-menu:action', action)
|
||||
},
|
||||
config: {
|
||||
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
|
||||
get: (key: string) => ipcRenderer.invoke('config:get', key)
|
||||
},
|
||||
miniWindow: {
|
||||
show: () => ipcRenderer.invoke('miniwindow:show'),
|
||||
hide: () => ipcRenderer.invoke('miniwindow:hide'),
|
||||
close: () => ipcRenderer.invoke('miniwindow:close'),
|
||||
toggle: () => ipcRenderer.invoke('miniwindow:toggle')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<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>
|
||||
html,
|
||||
body {
|
||||
@@ -16,10 +17,10 @@
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#spinner img {
|
||||
@@ -34,6 +35,7 @@
|
||||
<div id="spinner">
|
||||
<img src="/src/assets/images/logo.png" />
|
||||
</div>
|
||||
<script type="module" src="/src/init.ts"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ import { PersistGate } from 'redux-persist/integration/react'
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HistoryPage from './pages/history/HistoryPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
function App(): JSX.Element {
|
||||
@@ -23,23 +23,24 @@ function App(): JSX.Element {
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings" element={<PaintingsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/messages/*" element={<HistoryPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
<SyntaxHighlighterProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings" element={<PaintingsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</SyntaxHighlighterProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
|
||||
@@ -1,88 +1,91 @@
|
||||
@font-face {
|
||||
font-family: 'iconfont'; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1725606177995') format('woff2');
|
||||
font-family: "iconfont"; /* Project id 4753420 */
|
||||
src: url('iconfont.woff2?t=1736309723926') format('woff2'),
|
||||
url('iconfont.woff?t=1736309723926') format('woff'),
|
||||
url('iconfont.ttf?t=1736309723926') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: 'iconfont' !important;
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-at:before {
|
||||
content: "\e623";
|
||||
}
|
||||
|
||||
.icon-icon-adaptive-width:before {
|
||||
content: "\e87a";
|
||||
}
|
||||
|
||||
.icon-a-darkmode:before {
|
||||
content: '\e6cd';
|
||||
content: "\e6cd";
|
||||
}
|
||||
|
||||
.icon-ai-model:before {
|
||||
content: '\e827';
|
||||
content: "\e827";
|
||||
}
|
||||
|
||||
.icon-ai-model1:before {
|
||||
content: '\ec09';
|
||||
content: "\ec09";
|
||||
}
|
||||
|
||||
.icon-gridlines:before {
|
||||
content: '\e942';
|
||||
}
|
||||
|
||||
.icon-grid-row-2copy:before {
|
||||
content: '\e681';
|
||||
content: "\e942";
|
||||
}
|
||||
|
||||
.icon-inbox:before {
|
||||
content: '\e869';
|
||||
content: "\e869";
|
||||
}
|
||||
|
||||
.icon-business-smart-assistant:before {
|
||||
content: '\e601';
|
||||
content: "\e601";
|
||||
}
|
||||
|
||||
.icon-copy:before {
|
||||
content: '\e6ae';
|
||||
content: "\e6ae";
|
||||
}
|
||||
|
||||
.icon-ic_send:before {
|
||||
content: '\e795';
|
||||
content: "\e795";
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: '\e72f';
|
||||
content: "\e72f";
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: '\e6b7';
|
||||
content: "\e6b7";
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: '\e7de';
|
||||
content: "\e7de";
|
||||
}
|
||||
|
||||
.icon-history:before {
|
||||
content: '\e758';
|
||||
content: "\e758";
|
||||
}
|
||||
|
||||
.icon-hide-sidebar:before {
|
||||
content: '\e8eb';
|
||||
content: "\e8eb";
|
||||
}
|
||||
|
||||
.icon-show-sidebar:before {
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
.icon-a-addchat:before {
|
||||
content: '\e658';
|
||||
content: "\e944";
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: '\e792';
|
||||
content: "\e792";
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: '\e615';
|
||||
content: "\e615";
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: '\e78e';
|
||||
content: "\e78e";
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
src/renderer/src/assets/images/apps/duckduckgo.webp
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
4
src/renderer/src/assets/images/apps/flowith.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="464" height="464" viewBox="0 0 464 464" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="464" height="464" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M243 127C235.268 127 229 133.268 229 141V322C229 329.732 235.268 336 243 336H283C290.732 336 297 329.732 297 322V141C297 133.268 290.732 127 283 127H243ZM167.562 128C163.762 128 160.317 129.518 157.805 131.978C157.787 131.995 157.759 131.977 157.767 131.954C157.775 131.93 157.743 131.913 157.727 131.933L157.311 132.486C156.679 133.171 156.115 133.92 155.629 134.722C154.303 136.486 153.139 138.365 152.152 140.338L88.8745 266.857L85.2894 274.899C85.2249 275.037 85.1626 275.177 85.1027 275.318L84.7141 276.189C84.7086 276.201 84.7223 276.213 84.7339 276.206C84.745 276.2 84.7583 276.211 84.7541 276.223C84.2654 277.639 84 279.16 84 280.742L84 322.399C84 330.067 90.2354 336.284 97.9271 336.284H139.708C147.4 336.284 153.635 330.067 153.635 322.399V266.857L153.636 252.97C153.636 222.295 178.577 197.428 209.344 197.428C217.035 197.428 223.271 191.211 223.271 183.542V141.886C223.271 134.217 217.035 128 209.344 128H167.562ZM304.5 301.57C304.5 282.398 320.088 266.856 339.318 266.856C358.547 266.856 374.135 282.398 374.135 301.57C374.135 320.742 358.547 336.284 339.318 336.284C320.088 336.284 304.5 320.742 304.5 301.57Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/renderer/src/assets/images/apps/genspark.jpg
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/renderer/src/assets/images/apps/github-copilot.webp
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/renderer/src/assets/images/apps/grok.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/renderer/src/assets/images/apps/hika.webp
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/renderer/src/assets/images/apps/nm.webp
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/apps/qwenlm.webp
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/renderer/src/assets/images/apps/thinkany.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 84 KiB |
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ea5e5d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #23af69;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ea5756;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_图层_1-2" data-name="图层_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
|
||||
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
|
||||
</g>
|
||||
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
|
||||
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
|
||||
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
|
||||
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
|
||||
<g>
|
||||
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
|
||||
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
|
||||
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
|
||||
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
|
||||
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
|
||||
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
|
||||
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
|
||||
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
|
||||
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
|
||||
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
|
||||
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
|
||||
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.5 KiB |
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.39 115.44">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ea5e5d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #23af69;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ea5756;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_图层_1-2" data-name="图层_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1" d="M25.31,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
|
||||
<path class="cls-1" d="M40.64,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04c3.17-3.23,7.36-5.01,11.81-5.01s8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
|
||||
</g>
|
||||
<path class="cls-2" d="M40.64,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M10.19,90.22l.39-.28c.49-.29.83-.43,1.03-.43.45,0,.93.4,1.44,1.21.32.5.47.9.47,1.21s-.1.55-.29.75c-.19.2-.42.38-.68.54-.26.16-.51.31-.74.45-.24.14-.72.33-1.45.56-.73.23-1.44.34-2.12.34s-1.37-.09-2.07-.27c-.7-.18-1.41-.48-2.15-.9-.74-.42-1.4-.94-1.99-1.55-.59-.61-1.07-1.39-1.45-2.35-.38-.96-.57-1.99-.57-3.11s.19-2.14.56-3.05c.37-.91.85-1.67,1.43-2.26.58-.6,1.25-1.09,1.99-1.5,1.41-.78,2.82-1.16,4.24-1.16.67,0,1.36.1,2.06.31.7.21,1.22.42,1.58.64l.52.3c.26.16.46.29.6.39.37.3.56.64.56,1.02s-.15.78-.45,1.2c-.56.78-1.06,1.16-1.51,1.16-.26,0-.62-.16-1.1-.47-.6-.49-1.41-.73-2.41-.73-.93,0-1.85.32-2.76.97-.43.32-.79.76-1.08,1.34-.29.57-.43,1.22-.43,1.95s.14,1.37.43,1.95c.29.57.65,1.03,1.1,1.36.88.63,1.79.95,2.74.95.45,0,.87-.06,1.26-.17.39-.11.68-.23.85-.34Z"/>
|
||||
<path class="cls-3" d="M24.7,79.2c.11-.22.31-.37.58-.45.27-.09.62-.13,1.03-.13s.75.04.99.11c.24.07.43.16.56.26.13.1.23.24.3.43.07.24.11.62.11,1.12v11.95c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-4.37h-5.71v4.39c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v4.39h5.71v-4.42c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57Z"/>
|
||||
<path class="cls-3" d="M33.82,90.58h6.63c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-8.53c-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98c0-.73.14-1.23.41-1.5.27-.27.79-.4,1.55-.4h8.49c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-6.61v2.18h4.26c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.82,1.77-.24.09-.62.13-1.12.13h-4.22v2.18Z"/>
|
||||
<path class="cls-3" d="M83.34,79c.7.49,1.06.96,1.06,1.42,0,.27-.17.65-.5,1.14l-4.65,6.96v4.11c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.11.22-.31.37-.58.45-.27.09-.64.13-1.1.13s-.83-.04-1.1-.13c-.27-.09-.47-.24-.58-.46-.11-.22-.18-.42-.2-.58-.02-.17-.03-.42-.03-.76v-4.07l-4.65-6.96c-.33-.49-.5-.87-.5-1.14,0-.46.32-.9.95-1.32.63-.42,1.08-.64,1.36-.64s.49.06.65.17c.24.16.5.45.78.88l3.34,5.34,3.34-5.34c.27-.43.51-.71.71-.85s.43-.2.7-.2.69.18,1.26.54Z"/>
|
||||
<g>
|
||||
<path class="cls-3" d="M1.66,112.96c-.37-.46-.56-.87-.56-1.24s.31-.85.93-1.45c.36-.34.74-.52,1.14-.52s.96.36,1.68,1.08c.2.24.49.48.86.7.37.22.72.33,1.03.33,1.34,0,2-.55,2-1.64,0-.33-.18-.61-.55-.83-.37-.22-.82-.38-1.37-.48-.55-.1-1.13-.26-1.77-.48-.63-.22-1.22-.48-1.77-.79-.55-.3-1-.78-1.37-1.43-.37-.65-.55-1.44-.55-2.36,0-1.26.47-2.37,1.41-3.31s2.22-1.41,3.84-1.41c.86,0,1.65.11,2.36.33.71.22,1.2.45,1.48.68l.54.41c.45.42.67.77.67,1.06s-.17.68-.52,1.18c-.49.72-.99,1.08-1.51,1.08-.3,0-.67-.14-1.12-.43-.04-.03-.13-.1-.25-.22-.12-.11-.23-.21-.33-.28-.3-.19-.69-.28-1.15-.28s-.85.11-1.16.33c-.31.22-.46.53-.46.93s.18.71.55.96c.37.24.82.41,1.37.5.55.09,1.14.22,1.79.4.65.18,1.24.4,1.79.66.55.26,1,.71,1.37,1.35.37.64.55,1.42.55,2.36s-.19,1.76-.56,2.47c-.37.71-.86,1.26-1.46,1.65-1.16.76-2.4,1.14-3.73,1.14-.68,0-1.31-.08-1.92-.25-.6-.17-1.09-.37-1.46-.61-.76-.46-1.29-.9-1.59-1.34l-.19-.24Z"/>
|
||||
<path class="cls-3" d="M15.02,99.37h11.98c.46,0,.8.05,1.01.16.22.11.36.28.43.51.07.23.11.53.11.9s-.04.67-.11.89c-.07.22-.19.38-.37.46-.26.13-.62.19-1.1.19h-4.11v10.83c0,.33-.01.57-.03.73s-.09.34-.19.55c-.11.21-.3.36-.57.44-.27.09-.63.13-1.08.13s-.8-.04-1.07-.13c-.27-.09-.45-.23-.56-.44-.11-.21-.17-.4-.19-.56-.02-.17-.03-.41-.03-.74v-10.81h-4.14c-.46,0-.8-.05-1.01-.16-.22-.11-.36-.28-.43-.51-.07-.23-.11-.53-.11-.9s.04-.67.11-.89c.07-.22.19-.38.37-.46.26-.13.62-.19,1.1-.19Z"/>
|
||||
<path class="cls-3" d="M40.05,99.98c.14-.23.35-.39.62-.47.27-.09.61-.13,1.02-.13s.74.04.98.11c.24.07.43.16.56.26.13.1.22.25.28.45.09.24.13.62.13,1.12v6.5c0,1.9-.59,3.62-1.77,5.17-.57.73-1.31,1.32-2.22,1.78s-1.91.68-3,.68-2.1-.23-2.99-.69c-.9-.46-1.63-1.06-2.19-1.81-1.16-1.52-1.74-3.25-1.74-5.17v-6.48c0-.34.01-.6.03-.76.02-.17.09-.36.2-.57.11-.22.31-.37.58-.45.27-.09.64-.13,1.1-.13s.83.04,1.1.13c.27.09.46.24.56.45.17.33.26.78.26,1.36v6.46c0,.88.22,1.71.65,2.5.22.4.54.72.97.97.43.24.94.37,1.53.37,1.05,0,1.83-.39,2.35-1.16.52-.78.78-1.67.78-2.69v-6.59c0-.56.07-.95.22-1.18Z"/>
|
||||
<path class="cls-3" d="M47.28,99.37l3.98.02c2.08,0,3.91.75,5.49,2.25,1.58,1.5,2.37,3.35,2.37,5.54s-.77,4.07-2.32,5.63c-1.54,1.57-3.41,2.35-5.61,2.35h-3.94c-.88,0-1.42-.18-1.64-.54-.17-.3-.26-.76-.26-1.38v-11.98c0-.34.01-.6.03-.75s.09-.34.2-.56c.2-.39.76-.58,1.68-.58ZM51.27,111.35c1.03,0,1.97-.38,2.8-1.15.83-.77,1.25-1.73,1.25-2.9s-.41-2.14-1.22-2.92c-.81-.78-1.76-1.17-2.85-1.17h-2.07v8.14h2.09Z"/>
|
||||
<path class="cls-3" d="M60.53,101.29c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v11.98c0,.34-.01.6-.03.75s-.09.34-.2.56c-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98Z"/>
|
||||
<path class="cls-3" d="M73.47,99.2c2.13,0,3.97.77,5.54,2.3,1.57,1.54,2.35,3.44,2.35,5.72s-.75,4.21-2.24,5.82c-1.49,1.6-3.33,2.4-5.51,2.4s-4.04-.79-5.57-2.37c-1.53-1.58-2.29-3.46-2.29-5.64,0-1.19.22-2.31.65-3.35.43-1.04,1.01-1.91,1.72-2.62.72-.7,1.54-1.26,2.48-1.66.93-.4,1.9-.6,2.89-.6ZM69.55,107.32c0,1.28.41,2.32,1.24,3.11.83.8,1.75,1.2,2.77,1.2s1.94-.39,2.76-1.16c.82-.78,1.23-1.82,1.23-3.12s-.41-2.35-1.24-3.14c-.83-.79-1.75-1.18-2.77-1.18s-1.94.4-2.76,1.2c-.82.8-1.23,1.83-1.23,3.11Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M69.11,80.61c-.02-.17-.09-.36-.2-.57-.2-.39-.76-.58-1.68-.58h-4.65c-1.26,0-2.49.46-3.68,1.38-.57.45-1.05,1.05-1.42,1.81-.37.76-.56,1.61-.56,2.54,0,1.62.54,2.96,1.62,4.01-.32.76-.8,1.89-1.46,3.38-.22.52-.32.89-.32,1.12,0,.55.45,1.01,1.34,1.38.46.2.83.3,1.11.3s.51-.07.69-.2c.18-.14.31-.28.4-.42.14-.27.7-1.57,1.68-3.9l.67.04h2.71v2.43c0,.33.01.58.03.74.02.17.09.36.2.57.2.39.76.58,1.68.58,1.01,0,1.59-.27,1.77-.8.09-.24.13-.62.13-1.12v-11.95c0-.33-.01-.58-.03-.74ZM66.19,86.56c-.06.1-.34.54-.88.65-.14.03-.26.02-.34.02-.01.1-.03.19-.04.27-.11.59-.27.74-.4.76-.21.03-.36-.25-.66-.64-.16,0-.32.01-.49.01-.36,0-.72-.02-1.06-.06-.18-.02-.35-.05-.52-.08-.84-.22-1.44-.9-1.44-1.7v-.97c0-.8.61-1.48,1.44-1.7.17-.03.34-.06.52-.08.34-.04.69-.06,1.06-.06s.72.02,1.06.06c.18.02.35.05.52.08.43.12.8.35,1.06.66.24.29.39.65.39,1.05v.97c0,.16,0,.47-.21.78Z"/>
|
||||
<circle class="cls-2" cx="62.23" cy="85.3" r=".35"/>
|
||||
<circle class="cls-2" cx="63.4" cy="85.33" r=".35"/>
|
||||
<circle class="cls-2" cx="64.61" cy="85.33" r=".35"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M43.62,78.61c.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58h4.65c1.26,0,2.49.46,3.68,1.38.57.45,1.05,1.05,1.42,1.81.37.76.56,1.61.56,2.54,0,1.62-.54,2.96-1.62,4.01.32.76.8,1.89,1.46,3.38.22.52.32.89.32,1.12,0,.55-.45,1.01-1.34,1.38-.46.2-.83.3-1.11.3s-.51-.07-.69-.2c-.18-.14-.31-.28-.4-.42-.14-.27-.7-1.57-1.68-3.9l-.67.04h-2.71v2.43c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74ZM46.53,84.56c.06.1.34.54.88.65.14.03.26.02.34.02.01.1.03.19.04.27.11.59.27.74.4.76.21.03.36-.25.66-.64.16,0,.32.01.49.01.36,0,.72-.02,1.06-.06.18-.02.35-.05.52-.08.84-.22,1.44-.9,1.44-1.7v-.97c0-.8-.61-1.48-1.44-1.7-.17-.03-.34-.06-.52-.08-.34-.04-.69-.06-1.06-.06s-.72.02-1.06.06c-.18.02-.35.05-.52.08-.43.12-.8.35-1.06.66-.24.29-.39.65-.39,1.05v.97c0,.16,0,.47.21.78Z"/>
|
||||
<circle class="cls-2" cx="50.49" cy="83.3" r=".35"/>
|
||||
<circle class="cls-2" cx="49.32" cy="83.33" r=".35"/>
|
||||
<circle class="cls-2" cx="48.11" cy="83.33" r=".35"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.2 KiB |
BIN
src/renderer/src/assets/images/models/bge.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/renderer/src/assets/images/models/bigcode.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/renderer/src/assets/images/models/bigcode_dark.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
BIN
src/renderer/src/assets/images/models/jina.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/models/jina_dark.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/models/pixtral.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/models/pixtral_dark.png
Normal file
|
After Width: | Height: | Size: 915 B |
BIN
src/renderer/src/assets/images/providers/grok.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src/renderer/src/assets/images/providers/jina.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/providers/mistral.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/renderer/src/assets/images/providers/qwenlm.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
@@ -42,35 +42,6 @@
|
||||
-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 {
|
||||
.ant-upload-list-item:hover {
|
||||
background-color: initial !important;
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
--color-white-mute: rgba(255, 255, 255, 0.94);
|
||||
|
||||
--color-black: #151515;
|
||||
--color-black-soft: #202020;
|
||||
--color-black-mute: #262626;
|
||||
--color-black-soft: #222222;
|
||||
--color-black-mute: #333333;
|
||||
|
||||
--color-gray-1: #515c67;
|
||||
--color-gray-2: #414853;
|
||||
@@ -24,6 +24,7 @@
|
||||
--color-background: var(--color-black);
|
||||
--color-background-soft: var(--color-black-soft);
|
||||
--color-background-mute: var(--color-black-mute);
|
||||
--color-background-opacity: rgba(34, 34, 34, 0.7);
|
||||
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
@@ -32,14 +33,20 @@
|
||||
--color-text: var(--color-text-1);
|
||||
--color-icon: #ffffff99;
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #ffffff24;
|
||||
--color-border-soft: #ffffff20;
|
||||
--color-border: #ffffff22;
|
||||
--color-border-soft: #ffffff11;
|
||||
--color-border-mute: #ffffff11;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #323232;
|
||||
--color-hover: rgba(40, 40, 40, 1);
|
||||
--color-active: rgba(55, 55, 55, 1);
|
||||
--color-frame-border: #333;
|
||||
--color-group-background: var(--color-background-soft);
|
||||
|
||||
--color-reference: #404040;
|
||||
--color-reference-text: #ffffff;
|
||||
--color-reference-background: #0b0e12;
|
||||
|
||||
--navbar-background-mac: rgba(30, 30, 30, 0.6);
|
||||
--navbar-background: rgba(30, 30, 30);
|
||||
@@ -57,6 +64,8 @@
|
||||
--chat-background-user: #28b561;
|
||||
--chat-background-assistant: #2c2c2c;
|
||||
--chat-text-user: var(--color-black);
|
||||
|
||||
--list-item-border-radius: 16px;
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
@@ -79,6 +88,7 @@ body[theme-mode='light'] {
|
||||
--color-background: var(--color-white);
|
||||
--color-background-soft: var(--color-white-soft);
|
||||
--color-background-mute: var(--color-white-mute);
|
||||
--color-background-opacity: rgba(255, 255, 255, 0.7);
|
||||
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
@@ -88,13 +98,19 @@ body[theme-mode='light'] {
|
||||
--color-icon: #00000099;
|
||||
--color-icon-white: #000000;
|
||||
--color-border: #00000028;
|
||||
--color-border-soft: #00000028;
|
||||
--color-border-mute: #00000011;
|
||||
--color-border-soft: #00000020;
|
||||
--color-border-mute: #00000010;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #e3e3e3;
|
||||
--color-hover: var(--color-white-mute);
|
||||
--color-active: var(--color-white-soft);
|
||||
--color-frame-border: #ddd;
|
||||
--color-group-background: var(--color-white);
|
||||
|
||||
--color-reference: #cfe1ff;
|
||||
--color-reference-text: #000000;
|
||||
--color-reference-background: #f1f7ff;
|
||||
|
||||
--navbar-background-mac: rgba(255, 255, 255, 0.6);
|
||||
--navbar-background: rgba(255, 255, 255);
|
||||
@@ -163,20 +179,9 @@ body,
|
||||
#content-container {
|
||||
background-color: var(--color-background);
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
}
|
||||
|
||||
body[os='mac'] {
|
||||
#content-container {
|
||||
border-top-left-radius: 10px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
body[os='windows'] {
|
||||
#app-sidebar {
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
}
|
||||
border-top-left-radius: 10px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.loader {
|
||||
@@ -208,3 +213,50 @@ body[os='windows'] {
|
||||
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 {
|
||||
margin: -5px 15px 15px 15px;
|
||||
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-thought-container {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
}
|
||||
.group-message-wrapper {
|
||||
background-color: var(--color-background);
|
||||
.message-content-container {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-background-mute);
|
||||
}
|
||||
}
|
||||
code {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: initial;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1.5em;
|
||||
@@ -98,10 +102,6 @@
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
p code,
|
||||
li code {
|
||||
background: var(--color-background-mute);
|
||||
@@ -111,11 +111,18 @@
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
||||
background-color: var(--color-background-mute);
|
||||
&:has(> .mermaid) {
|
||||
background-color: transparent;
|
||||
}
|
||||
&:not(pre pre) {
|
||||
> code:not(pre pre > code) {
|
||||
padding: 15px;
|
||||
@@ -201,6 +208,14 @@
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-reference);
|
||||
color: var(--color-reference-text);
|
||||
padding: 2px 5px;
|
||||
zoom: 0.8;
|
||||
& > span.link {
|
||||
color: var(--color-reference-text);
|
||||
}
|
||||
}
|
||||
|
||||
sub {
|
||||
@@ -219,38 +234,55 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
margin-top: 1em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid var(--color-border);
|
||||
.footnotes {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
|
||||
ol {
|
||||
padding-left: 1em;
|
||||
background-color: var(--color-reference-background);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
ol {
|
||||
padding-left: 1em;
|
||||
margin: 0;
|
||||
li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--color-text-light);
|
||||
li {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--color-text-light);
|
||||
|
||||
p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-backref {
|
||||
font-size: 0.8em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
margin-left: 5px;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
.footnote-backref {
|
||||
font-size: 0.8em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
margin-left: 5px;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||