Compare commits
712 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94d9b79957 | ||
|
|
78a7b2759e | ||
|
|
27c0edfb79 | ||
|
|
59b1d8bcc4 | ||
|
|
741d84b4d3 | ||
|
|
5bacf048f2 | ||
|
|
1d4916c516 | ||
|
|
8e1207c2a2 | ||
|
|
ac92f1a783 | ||
|
|
28c59ea436 | ||
|
|
9e808208ab | ||
|
|
feefaaf3e3 | ||
|
|
31078b8ec5 | ||
|
|
f3f32cc591 | ||
|
|
f489b034b5 | ||
|
|
3d9d5b6263 | ||
|
|
89440c9c10 | ||
|
|
4c0f358323 | ||
|
|
ad01fc43e5 | ||
|
|
9b17416f9c | ||
|
|
cda4edfb7f | ||
|
|
acc803aa43 | ||
|
|
2ab8f325df | ||
|
|
a68cbe4438 | ||
|
|
646d0e4ccb | ||
|
|
a7a82be083 | ||
|
|
c0117c25ac | ||
|
|
d51da99b8f | ||
|
|
d4848faa5a | ||
|
|
bfeca0b383 | ||
|
|
79c7c3dc1c | ||
|
|
b2ebbc1e30 | ||
|
|
50e2dd0ec0 | ||
|
|
5e753de71c | ||
|
|
6bc6dab879 | ||
|
|
7d794d33dd | ||
|
|
aab318e8ca | ||
|
|
6554a3817b | ||
|
|
7d76db40e8 | ||
|
|
186c82e355 | ||
|
|
67311f1cbe | ||
|
|
62d969335e | ||
|
|
c6eb77ab8b | ||
|
|
7e17987fa3 | ||
|
|
4bc69b7c5e | ||
|
|
e08029a6f5 | ||
|
|
93d68102d6 | ||
|
|
f448d8a8db | ||
|
|
a047048f69 | ||
|
|
aec14567ee | ||
|
|
bad89e3d28 | ||
|
|
408f2b16ad | ||
|
|
d6b87ece23 | ||
|
|
91104e288c | ||
|
|
aeeded2aa1 | ||
|
|
b10198de1f | ||
|
|
0789ccedbb | ||
|
|
0a5401174b | ||
|
|
69513cc76e | ||
|
|
5a471125db | ||
|
|
06b2ca9149 | ||
|
|
26dd931f70 | ||
|
|
68df5cd211 | ||
|
|
c7071a98f0 | ||
|
|
ff14dcc559 | ||
|
|
bb02ca83dc | ||
|
|
4d9e842381 | ||
|
|
0165bcdce3 | ||
|
|
f015c78060 | ||
|
|
c233ba0a1c | ||
|
|
88dd75827a | ||
|
|
2ab63f2e4c | ||
|
|
91bf356c73 | ||
|
|
28c0748001 | ||
|
|
3108a1c0b3 | ||
|
|
6cfa7d0eb6 | ||
|
|
da6c80ebc2 | ||
|
|
af1a9868db | ||
|
|
f87ba144c8 | ||
|
|
8d61cbcae9 | ||
|
|
c61dde5085 | ||
|
|
3015e90925 | ||
|
|
b3629e83f2 | ||
|
|
cef9312e7e | ||
|
|
802622646a | ||
|
|
ce70c7239c | ||
|
|
c354537f30 | ||
|
|
fb6b0b0c97 | ||
|
|
fc59144b1d | ||
|
|
cacd0a1387 | ||
|
|
af9763d142 | ||
|
|
b9402a8370 | ||
|
|
97a08f00a3 | ||
|
|
4e20bd1ef8 | ||
|
|
5c2d936688 | ||
|
|
6b34aac263 | ||
|
|
f59f6ade69 | ||
|
|
50478c600f | ||
|
|
bba5cac246 | ||
|
|
b9b31aed52 | ||
|
|
40203fb721 | ||
|
|
b83343a8b9 | ||
|
|
7677850547 | ||
|
|
d0e233f1b3 | ||
|
|
903d0043ba | ||
|
|
811815d69d | ||
|
|
d5b9c35f0a | ||
|
|
83c8f06b81 | ||
|
|
acb2ea30fb | ||
|
|
55317b5608 | ||
|
|
b42a5c5e63 | ||
|
|
cc76fe19f9 | ||
|
|
cf2d7ba8b4 | ||
|
|
1c163c55b8 | ||
|
|
69bb661b5a | ||
|
|
f062c56de4 | ||
|
|
4c9bd02f8e | ||
|
|
241cb0c0d8 | ||
|
|
6a57973864 | ||
|
|
369f629206 | ||
|
|
09a8f83650 | ||
|
|
40912eaaf4 | ||
|
|
0d236a94ab | ||
|
|
3a936e0f26 | ||
|
|
81a35d129d | ||
|
|
e2d0c3bbce | ||
|
|
12c9d810a2 | ||
|
|
b18b161094 | ||
|
|
32da853f27 | ||
|
|
bf51a0b5c6 | ||
|
|
ae71a7be9e | ||
|
|
0fb6795833 | ||
|
|
dd6d228760 | ||
|
|
8c5999dc82 | ||
|
|
31b0fbf775 | ||
|
|
39fe583030 | ||
|
|
5c19695e21 | ||
|
|
6e4610e337 | ||
|
|
eb2439b90c | ||
|
|
a4a0980cd3 | ||
|
|
7d99765589 | ||
|
|
5f6cf1bd66 | ||
|
|
02930a2793 | ||
|
|
b31b1c7908 | ||
|
|
d0ee764732 | ||
|
|
01cd10b364 | ||
|
|
29ba156b9a | ||
|
|
e541c7b429 | ||
|
|
5f4142f0c4 | ||
|
|
95bbc70c93 | ||
|
|
4add56ae6a | ||
|
|
16f87537a2 | ||
|
|
7f05626a8f | ||
|
|
2094e2201a | ||
|
|
e0fcdf43c5 | ||
|
|
affc866c17 | ||
|
|
799267049f | ||
|
|
cb8d47a17b | ||
|
|
c494288f7b | ||
|
|
2c3f89dbde | ||
|
|
4721a660fa | ||
|
|
6aaa3def0d | ||
|
|
045708d9b3 | ||
|
|
9ffe92d378 | ||
|
|
7159481217 | ||
|
|
d07e136037 | ||
|
|
b38a9c954a | ||
|
|
7139d5093a | ||
|
|
9e283d6930 | ||
|
|
c9a4e12765 | ||
|
|
7bd644451b | ||
|
|
5a00bdcbc6 | ||
|
|
3c958c3d11 | ||
|
|
1d5ace0fb2 | ||
|
|
f8fce871da | ||
|
|
de76d3fedc | ||
|
|
b2c6662192 | ||
|
|
bf8a7c01b0 | ||
|
|
fb8ed35b59 | ||
|
|
7c4d81c108 | ||
|
|
7199f73e06 | ||
|
|
869e56b53c | ||
|
|
f99851fb6b | ||
|
|
c94450db44 | ||
|
|
195ef92acc | ||
|
|
a67370426b | ||
|
|
9d35205681 | ||
|
|
98087e50db | ||
|
|
bedac4f59d | ||
|
|
aba3874797 | ||
|
|
3383280726 | ||
|
|
0c13e708b9 | ||
|
|
bc77c423b3 | ||
|
|
4821756301 | ||
|
|
78290ca70e | ||
|
|
7feeb07624 | ||
|
|
93e28ed916 | ||
|
|
b4aaf052fe | ||
|
|
b37e0389fc | ||
|
|
e1ebe069a5 | ||
|
|
d73912ee3b | ||
|
|
f81c7c7a6c | ||
|
|
5a7bcd5997 | ||
|
|
09a347cae4 | ||
|
|
266f909045 | ||
|
|
bad2f15c1f | ||
|
|
e3115d00bf | ||
|
|
0c0ccf3d11 | ||
|
|
2076e6f998 | ||
|
|
b49d80b78d | ||
|
|
ab5e830ed1 | ||
|
|
e0eca97053 | ||
|
|
d175212d9a | ||
|
|
642ce160a1 | ||
|
|
574d02a8c9 | ||
|
|
7764507d74 | ||
|
|
fa8bf61532 | ||
|
|
30e8cef9cc | ||
|
|
1a2861e81a | ||
|
|
653e5d82ed | ||
|
|
5be0e0ae72 | ||
|
|
b92b46f2b0 | ||
|
|
23686d4926 | ||
|
|
b340b40bcf | ||
|
|
253fc6f4e1 | ||
|
|
99aa0d3255 | ||
|
|
23a2a6b57c | ||
|
|
a869857fc1 | ||
|
|
4ecedcb267 | ||
|
|
cbd6a30e14 | ||
|
|
5f2cddee09 | ||
|
|
c0e0e924f7 | ||
|
|
b6ad7eeb9a | ||
|
|
9cf74317a6 | ||
|
|
82fcc2292e | ||
|
|
4eb0c25682 | ||
|
|
9e128d2524 | ||
|
|
1473cb3123 | ||
|
|
2c5fe01fbf | ||
|
|
d574a09529 | ||
|
|
f20bccfd7d | ||
|
|
5dcc892f31 | ||
|
|
26e3871688 | ||
|
|
9a6aad35b0 | ||
|
|
16feb49e9e | ||
|
|
30959e2380 | ||
|
|
2c17f75f4f | ||
|
|
2d1a930bfe | ||
|
|
320d27059f | ||
|
|
31014aa8a6 | ||
|
|
b468ecfce7 | ||
|
|
c53d63f7af | ||
|
|
dabff0a847 | ||
|
|
26a5ae0086 | ||
|
|
88e0d293a2 | ||
|
|
0c97b52c53 | ||
|
|
2449a22c69 | ||
|
|
028f9d88d9 | ||
|
|
a07c6cdffb | ||
|
|
5a647b0d61 | ||
|
|
007e6419ba | ||
|
|
caa473639c | ||
|
|
b6825a6ea2 | ||
|
|
710180997f | ||
|
|
fd4334f331 | ||
|
|
80dedc149a | ||
|
|
8eacaa281a | ||
|
|
6e75140939 | ||
|
|
5a3a97135f | ||
|
|
44d42d64ef | ||
|
|
fad3f67678 | ||
|
|
65b30b3b0d | ||
|
|
0278228a84 | ||
|
|
bb0cb1cecc | ||
|
|
f5cd6ecb50 | ||
|
|
76c0ad9985 | ||
|
|
4c771c6913 | ||
|
|
15d50761e7 | ||
|
|
1c33c90884 | ||
|
|
53e5a3bf76 | ||
|
|
adfea253d7 | ||
|
|
6c17fee69e | ||
|
|
40958ffb2c | ||
|
|
22d2121dcc | ||
|
|
4632f1a92a | ||
|
|
b9affe3eb8 | ||
|
|
c6f136caa2 | ||
|
|
827959e580 | ||
|
|
8739c49634 | ||
|
|
e99f253d48 | ||
|
|
0cb82bd3bd | ||
|
|
cf89981b64 | ||
|
|
e4133b0054 | ||
|
|
9249ee9094 | ||
|
|
0a4b360745 | ||
|
|
59c4d60d6a | ||
|
|
dcdb00eee7 | ||
|
|
946129ceb3 | ||
|
|
ada45b229d | ||
|
|
7934ce473d | ||
|
|
f749bef2fd | ||
|
|
6b2452422e | ||
|
|
50438dd612 | ||
|
|
38f665e484 | ||
|
|
635e125ef4 | ||
|
|
1b3ae92854 | ||
|
|
ce66f2e2ea | ||
|
|
726efe3558 | ||
|
|
24deb56d00 | ||
|
|
5e8d7682f5 | ||
|
|
ceb97e80ff | ||
|
|
bf1fa5b767 | ||
|
|
e99f34893e | ||
|
|
a49dd6101e | ||
|
|
d03b852671 | ||
|
|
13e48411c1 | ||
|
|
de1976d984 | ||
|
|
220046cc95 | ||
|
|
bae76f921b | ||
|
|
cb88a48d8b | ||
|
|
4d13a8d9c2 | ||
|
|
50cc1c6b5a | ||
|
|
1d82552491 | ||
|
|
5c2129c0c8 | ||
|
|
0eead315d8 | ||
|
|
04cfe5019e | ||
|
|
a0cfe7df4a | ||
|
|
3d8748a61a | ||
|
|
f3940159b3 | ||
|
|
81d4accacf | ||
|
|
c900a186b7 | ||
|
|
72d61ef853 | ||
|
|
8c32f51892 | ||
|
|
0a3ad04f12 | ||
|
|
555a991a30 | ||
|
|
6ba6357d21 | ||
|
|
cb3db57d2f | ||
|
|
e1a04030b5 | ||
|
|
220d11a414 | ||
|
|
3d2e209550 | ||
|
|
ee46d2055a | ||
|
|
3049023266 | ||
|
|
6fb79c17d2 | ||
|
|
9efc196ec5 | ||
|
|
186a1612e8 | ||
|
|
22920204d1 | ||
|
|
d4efbbb1bf | ||
|
|
3f7e84e17c | ||
|
|
6df0b02e49 | ||
|
|
280ec3377b | ||
|
|
6a30eec5b4 | ||
|
|
a6497b8c98 | ||
|
|
acda36ae3f | ||
|
|
496b4684ea | ||
|
|
2898215a00 | ||
|
|
762c3d4950 | ||
|
|
cec5eb3989 | ||
|
|
61ebe97a5a | ||
|
|
609a80bf2d | ||
|
|
468423040f | ||
|
|
85efc6e96b | ||
|
|
9f19493b41 | ||
|
|
3e43887be8 | ||
|
|
bffc9d23aa | ||
|
|
402a3e5ef9 | ||
|
|
ce2c8c7495 | ||
|
|
dc6d79366e | ||
|
|
777455f167 | ||
|
|
65cc51ea94 | ||
|
|
95936dca2a | ||
|
|
9aa829e6fc | ||
|
|
a27150c154 | ||
|
|
9acae0a728 | ||
|
|
9024d48938 | ||
|
|
02080954bc | ||
|
|
abb922a2b1 | ||
|
|
a1b88758cc | ||
|
|
3d16c735d9 | ||
|
|
e74391562b | ||
|
|
53f46218d3 | ||
|
|
333547df3d | ||
|
|
4c877fb0a3 | ||
|
|
bfa61ae3ee | ||
|
|
2208ab7277 | ||
|
|
507efda688 | ||
|
|
0914df8908 | ||
|
|
205aa70825 | ||
|
|
6c51e1d756 | ||
|
|
8e58dab337 | ||
|
|
e171715490 | ||
|
|
bda00e0a90 | ||
|
|
b56d00a7e2 | ||
|
|
e520db6949 | ||
|
|
de141c8127 | ||
|
|
3f3259784b | ||
|
|
66c2c530c5 | ||
|
|
d12fc29515 | ||
|
|
44991edfbd | ||
|
|
d33714ad68 | ||
|
|
e50223d219 | ||
|
|
4128b075e1 | ||
|
|
a1f0c039b1 | ||
|
|
76abe08d1b | ||
|
|
1355dfe744 | ||
|
|
791d29cc41 | ||
|
|
88e76da466 | ||
|
|
250aa7154a | ||
|
|
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 | ||
|
|
a4d8e71916 | ||
|
|
39cf227e42 | ||
|
|
d2cad31db4 | ||
|
|
aa864f3876 | ||
|
|
77a8b23d76 |
@@ -16,6 +16,7 @@ module.exports = {
|
||||
'react/prop-types': 'off',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'react/no-is-mounted': 'off'
|
||||
'react/no-is-mounted': 'off',
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }]
|
||||
}
|
||||
}
|
||||
|
||||
88
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: 🐛 错误报告 (中文)
|
||||
description: 创建一个报告以帮助我们改进
|
||||
title: '[错误]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间填写此错误报告!
|
||||
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||
required: true
|
||||
|
||||
- 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: 任何能让我们对你所遇到的问题有更多了解的东西
|
||||
76
.github/ISSUE_TEMPLATE/#1_feature_request.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: 💡 功能建议 (中文)
|
||||
description: 为项目提出新的想法
|
||||
title: '[功能]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间提出新的功能建议!
|
||||
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的建议。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||
required: true
|
||||
- label: 最新的 Cherry Studio 版本没有实现我所提出的功能。
|
||||
required: true
|
||||
|
||||
- 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: problem
|
||||
attributes:
|
||||
label: 您的功能建议是否与某个问题/issue相关?
|
||||
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: 在此添加任何其他与功能建议相关的上下文或截图
|
||||
77
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: ❓ 讨论 & 提问 (中文)
|
||||
description: 寻求帮助、讨论问题、提出疑问等...
|
||||
title: '[讨论]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的提问!请尽可能详细地描述您的问题,这样我们才能更好地帮助您。
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Issue 检查清单
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议。
|
||||
required: true
|
||||
|
||||
- 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: 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
|
||||
90
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: 🐛 Bug Report (English)
|
||||
description: Create a report to help us improve
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill out this bug report!
|
||||
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: |
|
||||
Before submitting an issue, please make sure you have completed the following steps
|
||||
options:
|
||||
- label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
|
||||
required: true
|
||||
- label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found.
|
||||
required: true
|
||||
- label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
|
||||
required: true
|
||||
|
||||
- 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: Please be as detailed as possible when describing the problem. Please provide screenshots or screen recordings whenever possible to help us better understand the issue.
|
||||
placeholder: Tell us what happened... (Remember to attach screenshots/recordings if applicable)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Provide detailed steps to reproduce the issue so that our developers can reproduce the issue accurately. Please include screenshots or screen recordings for each step when possible.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
Remember to attach screenshots/recordings for each step when possible!
|
||||
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: Anything that gives us a better understanding of the problem you're experiencing
|
||||
76
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: 💡 Feature Request (English)
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to submit a feature request!
|
||||
Before submitting this issue, please make sure you have reviewed the [Project Roadmap](https://docs.cherry-ai.com/cherrystudio/planning) and the [Feature Overview](https://docs.cherry-ai.com/cherrystudio/preview).
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: |
|
||||
Before submitting an issue, please make sure you have completed the following steps
|
||||
options:
|
||||
- label: I understand that issues are for reporting problems and requesting features, not for off-topic comments, and I will provide as much detail as possible to help resolve the issue.
|
||||
required: true
|
||||
- label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion.
|
||||
required: true
|
||||
- label: I have provided a short and descriptive title so that developers can quickly understand the issue when browsing the issue list, rather than vague titles like "A suggestion" or "Stuck."
|
||||
required: true
|
||||
- label: The latest version of Cherry Studio does not include the feature I am suggesting.
|
||||
required: true
|
||||
|
||||
- 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: problem
|
||||
attributes:
|
||||
label: Is your feature request related to an existing issue?
|
||||
description: Please briefly describe the problem you are experiencing. If possible, include screenshots or recordings to help illustrate the current situation or pain points.
|
||||
placeholder: I often feel frustrated because... (Remember to attach screenshots/recordings if applicable)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Desired Solution
|
||||
description: Please briefly describe what you would like to happen. You can include mockups, screenshots, or screen recordings to better illustrate your proposed solution.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternative Solutions
|
||||
description: Please briefly describe any alternative solutions or features you have considered. Feel free to include screenshots or mockups of alternative approaches.
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Add any other context, screenshots, mockups or recordings that can help us better understand your feature request.
|
||||
79
.github/ISSUE_TEMPLATE/2_question.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: ❓ Discussion & Questions
|
||||
description: Seeking help, discussing issues, asking questions, etc...
|
||||
title: '[Discussion]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for your question! Please describe your issue in as much detail as possible so that we can better assist you.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: |
|
||||
Before submitting an issue, please make sure you have completed the following steps
|
||||
options:
|
||||
- label: I understand that issues are meant for feedback and problem-solving, not for venting, and I will provide as much detail as possible to help resolve the issue.
|
||||
required: true
|
||||
- label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion.
|
||||
required: true
|
||||
- label: I confirm that I am here to ask questions and discuss issues, not to report bugs or request features.
|
||||
required: true
|
||||
|
||||
- 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: question
|
||||
attributes:
|
||||
label: Your Question
|
||||
description: Please describe your issue in detail. Include screenshots or screen recordings whenever possible to help us better understand your question.
|
||||
placeholder: Please explain your issue as clearly as possible...(Remember to attach screenshots/recordings if applicable)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Context
|
||||
description: Please provide some background information to help us better understand your question. Screenshots or recordings of your current setup or situation can be very helpful.
|
||||
placeholder: "For example: use case, solutions you've tried, etc. Don't forget to include relevant screenshots/recordings!"
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other relevant information, screenshots, recordings, or code examples that can help us better assist you
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How urgent is this issue for you?
|
||||
options:
|
||||
- Low (Review when available)
|
||||
- Medium (Would like a response soon)
|
||||
- High (Blocking progress)
|
||||
validations:
|
||||
required: true
|
||||
73
.github/workflows/release.yml
vendored
@@ -3,10 +3,10 @@ name: Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version (e.g. v1.2.3)'
|
||||
tag:
|
||||
description: 'Release tag (e.g. v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
default: 'v0.9.18'
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
@@ -21,10 +21,21 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get release tag
|
||||
id: get-tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
@@ -34,49 +45,63 @@ 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 }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
|
||||
- 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 }}
|
||||
APPLE_ID: ${{ vars.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: yarn build:win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
|
||||
- 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
|
||||
tag: ${{ steps.get-tag.outputs.tag }}
|
||||
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.GITHUB_TOKEN }}
|
||||
|
||||
4
.gitignore
vendored
@@ -36,6 +36,7 @@ node_modules
|
||||
dist
|
||||
out
|
||||
build/icons
|
||||
stats.html
|
||||
|
||||
# ENV
|
||||
.env
|
||||
@@ -43,3 +44,6 @@ build/icons
|
||||
|
||||
# Local
|
||||
local
|
||||
.aider*
|
||||
.cursorrules
|
||||
.cursor/rules
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
19
.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch
Normal file
@@ -0,0 +1,19 @@
|
||||
diff --git a/dist/embeddings.js b/dist/embeddings.js
|
||||
index 1f8154be3e9c22442a915eb4b85fa6d2a21b0d0c..dc13ef4a30e6c282824a5357bcee9bd0ae222aab 100644
|
||||
--- a/dist/embeddings.js
|
||||
+++ b/dist/embeddings.js
|
||||
@@ -214,10 +214,12 @@ export class OpenAIEmbeddings extends Embeddings {
|
||||
* @returns Promise that resolves to an embedding for the document.
|
||||
*/
|
||||
async embedQuery(text) {
|
||||
+ const isBaiduCloud = this.clientConfig.baseURL.includes('baidubce.com')
|
||||
+ const input = this.stripNewLines ? text.replace(/\n/g, ' ') : text
|
||||
const params = {
|
||||
model: this.model,
|
||||
- input: this.stripNewLines ? text.replace(/\n/g, " ") : text,
|
||||
- };
|
||||
+ input: isBaiduCloud ? [input] : input
|
||||
+ }
|
||||
if (this.dimensions) {
|
||||
params.dimensions = this.dimensions;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
|
||||
index eaf30b114a273e68abbb92c8b07018495e63f4cb..4b06519bdb51845e4693fe877da9de01c7a81039 100644
|
||||
--- a/src/markdown-loader.js
|
||||
+++ b/src/markdown-loader.js
|
||||
@@ -21,7 +21,7 @@ export class MarkdownLoader extends BaseLoader {
|
||||
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
|
||||
: await streamToBuffer(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,
|
||||
158
.yarn/patches/@llm-tools-embedjs-npm-0.1.28-8e4393fa2d.patch
Normal file
@@ -0,0 +1,158 @@
|
||||
diff --git a/src/loaders/local-path-loader.d.ts b/src/loaders/local-path-loader.d.ts
|
||||
index 48c20e68c469cd309be2dc8f28e44c1bd04a26e9..1c16d83bcbf9b7140292793d6cbb8c04281949d9 100644
|
||||
--- a/src/loaders/local-path-loader.d.ts
|
||||
+++ b/src/loaders/local-path-loader.d.ts
|
||||
@@ -4,8 +4,10 @@ export declare class LocalPathLoader extends BaseLoader<{
|
||||
}> {
|
||||
private readonly debug;
|
||||
private readonly path;
|
||||
- constructor({ path }: {
|
||||
+ constructor({ path, chunkSize, chunkOverlap }: {
|
||||
path: string;
|
||||
+ chunkSize?: number;
|
||||
+ chunkOverlap?: number;
|
||||
});
|
||||
getUnfilteredChunks(): AsyncGenerator<{
|
||||
metadata: {
|
||||
diff --git a/src/loaders/local-path-loader.js b/src/loaders/local-path-loader.js
|
||||
index 4cf8a6bd1d890244c8ec49d4a05ee3bd58861c79..ec8215b01195a21ef20f3c5d56ecc99f186bb596 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..14be3b5727cff6eb1978838045e9a788f8f53bfb 100644
|
||||
--- a/src/util/mime.d.ts
|
||||
+++ b/src/util/mime.d.ts
|
||||
@@ -1,2 +1,2 @@
|
||||
import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
-export declare function createLoaderFromMimeType(loaderData: string, mimeType: string): Promise<BaseLoader>;
|
||||
+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 b6426a859968e2bf6206795f70333e90ae27aeb7..16ae2adb863f8d7abfa757f1c5cc39f6bb1c44fa 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,24 @@ 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
|
||||
- return new TextLoader({ text: loaderData });
|
||||
+ const content = fs.readFileSync(loaderData, 'utf-8');
|
||||
+ return new TextLoader({ text: content, chunkSize, chunkOverlap });
|
||||
}
|
||||
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 +69,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 +84,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 'image/png':
|
||||
case 'image/jpeg': {
|
||||
57
.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch
Normal file
@@ -0,0 +1,57 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index 88c405a000d21b3631eaa378690907c5527b8eaf..e03e66440c7c93aee38adf57df3096c6fefcd96d 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -82,7 +82,6 @@ module.exports = __toCommonJS(index_exports);
|
||||
|
||||
// src/utils.ts
|
||||
var import_axios = __toESM(require("axios"));
|
||||
-var import_js_tiktoken = require("js-tiktoken");
|
||||
var BASE_URL = "https://api.tavily.com";
|
||||
var DEFAULT_MODEL_ENCODING = "gpt-3.5-turbo";
|
||||
var DEFAULT_MAX_TOKENS = 4e3;
|
||||
@@ -97,8 +96,7 @@ function post(endpoint, body, apiKey) {
|
||||
});
|
||||
}
|
||||
function getTotalTokensFromString(str, encodingName = DEFAULT_MODEL_ENCODING) {
|
||||
- const encoding = (0, import_js_tiktoken.encodingForModel)(encodingName);
|
||||
- return encoding.encode(str).length;
|
||||
+ return 0;
|
||||
}
|
||||
function getMaxTokensFromList(data, maxTokens = DEFAULT_MAX_TOKENS) {
|
||||
var result = [];
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 0a9ea6a0add8d709e6721e806571f373d9fe0487..b81f1ea48a2b2a30ee98d53980a1b04ea3fdc5d4 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -49,7 +49,6 @@ var __async = (__this, __arguments, generator) => {
|
||||
|
||||
// src/utils.ts
|
||||
import axios from "axios";
|
||||
-import { encodingForModel } from "js-tiktoken";
|
||||
var BASE_URL = "https://api.tavily.com";
|
||||
var DEFAULT_MODEL_ENCODING = "gpt-3.5-turbo";
|
||||
var DEFAULT_MAX_TOKENS = 4e3;
|
||||
@@ -64,8 +63,7 @@ function post(endpoint, body, apiKey) {
|
||||
});
|
||||
}
|
||||
function getTotalTokensFromString(str, encodingName = DEFAULT_MODEL_ENCODING) {
|
||||
- const encoding = encodingForModel(encodingName);
|
||||
- return encoding.encode(str).length;
|
||||
+ return 0;
|
||||
}
|
||||
function getMaxTokensFromList(data, maxTokens = DEFAULT_MAX_TOKENS) {
|
||||
var result = [];
|
||||
diff --git a/package.json b/package.json
|
||||
index 36d4a613166a7906c1dc5377a89dc0a65f746f73..dc6e0e9363046755cad123e627cc270a2e3580d1 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -36,7 +36,6 @@
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
- "axios": "^1.7.7",
|
||||
- "js-tiktoken": "^1.0.14"
|
||||
+ "axios": "^1.7.7"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
diff --git a/core.js b/core.js
|
||||
index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d82573ea5 100644
|
||||
--- a/core.js
|
||||
+++ b/core.js
|
||||
@@ -156,7 +156,7 @@ class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
diff --git a/core.mjs b/core.mjs
|
||||
index 8bc7a0ee10d61560d7113cf3f703355bb19f7ddd..5e4c8586ea6b13fe887a22af2de05eaa4700b5ec 100644
|
||||
--- a/core.mjs
|
||||
+++ b/core.mjs
|
||||
@@ -149,7 +149,7 @@ export class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
39
.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch
Normal file
@@ -0,0 +1,39 @@
|
||||
diff --git a/core.js b/core.js
|
||||
index e75a18281ce8f051990c5a50bc1076afdddf91a3..e62f796791a155f23d054e74a429516c14d6e11b 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 fcef58eb502664c41a77483a00db8adaf29b2817..18c5d6ed4be86b3640931277bdc27700006764d7 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),
|
||||
};
|
||||
}
|
||||
diff --git a/error.mjs b/error.mjs
|
||||
index 7d19f5578040afa004bc887aab1725e8703d2bac..59ec725b6142299a62798ac4bdedb63ba7d9932c 100644
|
||||
--- a/error.mjs
|
||||
+++ b/error.mjs
|
||||
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
|
||||
if (!status || !headers) {
|
||||
return new APIConnectionError({ message, cause: castToError(errorResponse) });
|
||||
}
|
||||
- const error = errorResponse?.['error'];
|
||||
+ const error = errorResponse?.['error'] || errorResponse;
|
||||
if (status === 400) {
|
||||
return new BadRequestError(status, error, message, headers);
|
||||
}
|
||||
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;
|
||||
|
||||
38
README.md
@@ -1,19 +1,20 @@
|
||||
<div align="center">
|
||||
<h1 align="center">
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/releases">
|
||||
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
|
||||
</a>
|
||||
</div>
|
||||
</h1>
|
||||
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a><br></p>
|
||||
<div align="center">
|
||||
English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a>
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
# 🍒 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/wez8HtpxqQ) | [QQ Group(1025067911)](https://qm.qq.com/q/RIBAO2pPKS)
|
||||
|
||||
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
|
||||
|
||||
# 🌠 Screenshot
|
||||
|
||||
@@ -23,11 +24,13 @@ 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
|
||||
- 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others
|
||||
- 💻 Local Model Support with Ollama
|
||||
- 💻 Local Model Support with Ollama, LM Studio
|
||||
|
||||
2. **AI Assistants & Conversations**:
|
||||
|
||||
@@ -57,6 +60,21 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
||||
- 📝 Complete Markdown Rendering
|
||||
- 🤲 Easy Content Sharing
|
||||
|
||||
# 📝 TODO
|
||||
|
||||
- [x] Quick popup (read clipboard, quick question, explain, translate, summarize)
|
||||
- [x] Comparison of multi-model answers
|
||||
- [x] Support login using SSO provided by service providers
|
||||
- [x] All models support networking
|
||||
- [x] Launch of the first official version
|
||||
- [x] Bug fixes and improvements (In progress...)
|
||||
- [ ] Plugin functionality (JavaScript)
|
||||
- [ ] Browser extension (highlight text to translate, summarize, add to knowledge base)
|
||||
- [ ] iOS & Android client
|
||||
- [ ] AI notes
|
||||
- [ ] Voice input and output (AI call)
|
||||
- [ ] Data backup supports custom backup content
|
||||
|
||||
# 🖥️ Develop
|
||||
|
||||
## IDE Setup
|
||||
@@ -113,6 +131,10 @@ For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIB
|
||||
|
||||
Thank you for your support and contributions!
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution.
|
||||
|
||||
# 🚀 Contributors
|
||||
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
|
||||
|
||||
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
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.0 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
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<div align="center">
|
||||
<h1 align="center">
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/releases">
|
||||
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||
</a>
|
||||
</div>
|
||||
</h1>
|
||||
<div align="center">
|
||||
<a href="./README.md">English</a> | <a href="./README.zh.md">中文</a> | 日本語
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

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

|
||||
|
||||
1. **多様な LLM サービス対応**:
|
||||
|
||||
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
|
||||
- 🔗 AI Web サービス統合:Claude、Peplexity、Poe など
|
||||
- 💻 Ollama によるローカルモデル実行対応
|
||||
- 💻 Ollama、LM Studio によるローカルモデル実行対応
|
||||
|
||||
2. **AI アシスタントと対話**:
|
||||
|
||||
@@ -57,6 +61,21 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
|
||||
- 📝 完全な Markdown レンダリング
|
||||
- 🤲 簡単な共有機能
|
||||
|
||||
# 📝 TODO
|
||||
|
||||
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
|
||||
- [x] 複数モデルの回答の比較
|
||||
- [x] サービスプロバイダーが提供するSSOを使用したログインをサポート
|
||||
- [x] すべてのモデルがネットワークをサポート
|
||||
- [x] 最初の公式バージョンのリリース
|
||||
- [ ] 錯誤修復と改善 (開発中...)
|
||||
- [ ] プラグイン機能(JavaScript)
|
||||
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
|
||||
- [ ] iOS & Android クライアント
|
||||
- [ ] AIノート
|
||||
- [ ] 音声入出力(AIコール)
|
||||
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
|
||||
|
||||
# 🖥️ 開発
|
||||
|
||||
## IDEの設定
|
||||
@@ -113,6 +132,10 @@ Cherry Studioへの貢献を歓迎します!以下の方法で貢献できま
|
||||
|
||||
ご支援と貢献に感謝します!
|
||||
|
||||
## 関連頁版
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api):LLM APIの管理・配信システム。OpenAI、Azure、Anthropicなどの主要モデルに対応し、統一APIインターフェースを提供。APIキー管理と再配布に利用可能。
|
||||
|
||||
# 🚀 コントリビューター
|
||||
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<div align="center">
|
||||
<h1 align="center">
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/releases">
|
||||
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||
</a>
|
||||
</div>
|
||||
</h1>
|
||||
<div align="center">
|
||||
中文 / <a href="https://github.com/kangfenmao/cherry-studio">English</a> / <a href="./README.ja.md">日本語</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

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

|
||||
|
||||
1. **多样化 LLM 服务支持**:
|
||||
|
||||
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||
- 🔗 集成流行 AI Web 服务:Claude、Peplexity、Poe、腾讯元宝、知乎直答等
|
||||
- 💻 支持 Ollama 本地模型部署
|
||||
- 💻 支持 Ollama、LM Studio 本地模型部署
|
||||
|
||||
2. **智能助手与对话**:
|
||||
|
||||
@@ -57,6 +61,21 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
||||
- 📝 完整的 Markdown 渲染
|
||||
- 🤲 便捷的内容分享功能
|
||||
|
||||
# 📝 待辦事項
|
||||
|
||||
- [x] 快捷弹窗 (读取剪贴板、快速提问、解释、翻译、总结)
|
||||
- [x] 多模型回答对比
|
||||
- [x] 支持使用服务供应商提供的 SSO 进行登入
|
||||
- [x] 全部模型支持连网(开发中...)
|
||||
- [x] 推出第一个正式版
|
||||
- [x] 错误修复和改进(开发中...)
|
||||
- [ ] 插件功能(JavaScript)
|
||||
- [ ] 浏览器插件(划词翻译、总结、新增至知识库)
|
||||
- [ ] iOS & Android 客户端
|
||||
- [ ] AI 笔记
|
||||
- [ ] 语音输入输出(AI 通话)
|
||||
- [ ] 数据备份支持自定义备份内容
|
||||
|
||||
# 🖥️ 开发
|
||||
|
||||
## IDE 设置
|
||||
@@ -113,6 +132,10 @@ $ yarn build:linux
|
||||
|
||||
感谢您的支持和贡献!
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api):LLM API管理及分发系统,支持OpenAI、Azure、Anthropic等主流模型,统一API接口,可用于密钥管理与二次分发。
|
||||
|
||||
# 🚀 贡献者
|
||||
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
|
||||
|
||||
@@ -11,10 +11,32 @@ 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/@tavily/core/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 +44,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,32 +63,27 @@ 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: |
|
||||
支持清除应用缓存
|
||||
支持编辑翻译模型提示词
|
||||
支持使用搜索内容快速创建助手
|
||||
支持编辑模型是否为视觉模型
|
||||
界面样式优化和错误修复
|
||||
服务商新增天翼云
|
||||
助手支持使用模型图标当作头像
|
||||
网络搜索网站图标快速加载
|
||||
macOS 移除 Option 开头快捷键限制
|
||||
代理配置优化
|
||||
界面优化和错误修复
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
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',
|
||||
'@llm-tools/embedjs-loader-image'
|
||||
]
|
||||
}),
|
||||
...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,14 +43,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-KNVOMWSO.js']
|
||||
exclude: ['chunk-PZ64DZKH.js', 'chunk-JMKENWIY.js', 'chunk-UXYB6GHG.js']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
74
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "0.8.19",
|
||||
"version": "1.0.2",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -11,9 +11,11 @@
|
||||
"local",
|
||||
"packages/*"
|
||||
],
|
||||
"nohoist": [
|
||||
"installConfig": {
|
||||
"hoistingLimits": [
|
||||
"packages/database"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
@@ -23,31 +25,59 @@
|
||||
"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",
|
||||
"check": "node scripts/check-i18n.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@electron/notarize": "^2.5.0",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.28-8e4393fa2d.patch",
|
||||
"@llm-tools/embedjs-libsql": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-csv": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.28-81647ffac6.patch",
|
||||
"@llm-tools/embedjs-loader-msoffice": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-pdf": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-sitemap": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-web": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-xml": "^0.1.28",
|
||||
"@llm-tools/embedjs-openai": "^0.1.28",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@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",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"epub": "^1.3.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"tokenx": "^0.4.1",
|
||||
"undici": "^7.3.0",
|
||||
"webdav": "4.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -55,31 +85,36 @@
|
||||
"@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",
|
||||
"@llm-tools/embedjs-loader-image": "^0.1.28",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
"@types/adm-zip": "^0",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^18.19.9",
|
||||
"@types/pako": "^1.0.2",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@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",
|
||||
@@ -87,16 +122,16 @@
|
||||
"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": "patch:openai@npm%3A4.71.1#~/.yarn/patches/openai-npm-4.71.1-b5940d6401.patch",
|
||||
"openai": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.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",
|
||||
@@ -105,12 +140,14 @@
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-mathjax": "^6.0.0",
|
||||
"rehype-mathjax": "^7.0.0",
|
||||
"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",
|
||||
"string-width": "^7.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^5.6.2",
|
||||
@@ -122,7 +159,10 @@
|
||||
"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",
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0"
|
||||
"packageManager": "yarn@4.6.0"
|
||||
}
|
||||
|
||||
130
packages/shared/config/constant.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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 thirdPartyApplicationExts = ['.draftsExport']
|
||||
export const bookExts = ['.epub']
|
||||
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', // 富文本格式文件
|
||||
'.org', // org-mode 文件
|
||||
'.wiki', // VimWiki 文件
|
||||
'.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 脚本文件
|
||||
'.ipynb', // Jupyter 笔记本格式
|
||||
'.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# 代码文件
|
||||
'.cpp', // C++ 代码文件
|
||||
'.c', // C++ 代码文件
|
||||
'.h', // C++ 头文件
|
||||
'.hpp', // C++ 头文件
|
||||
'.cc', // C++ 源文件
|
||||
'.cxx', // C++ 源文件
|
||||
'.cppm', // C++20 模块接口文件
|
||||
'.ipp', // 模板实现文件
|
||||
'.ixx' // C++20 模块实现文件
|
||||
]
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
6
packages/shared/config/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type LoaderReturn = {
|
||||
entriesAdded: number
|
||||
uniqueId: string
|
||||
uniqueIds: string[]
|
||||
loaderType: string
|
||||
}
|
||||
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)
|
||||
104
scripts/check-i18n.js
Normal file
@@ -0,0 +1,104 @@
|
||||
'use strict'
|
||||
Object.defineProperty(exports, '__esModule', { value: true })
|
||||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
var baseLocale = 'zh-CN'
|
||||
var baseFileName = ''.concat(baseLocale, '.json')
|
||||
var baseFilePath = path.join(translationsDir, baseFileName)
|
||||
/**
|
||||
* 递归同步 target 对象,使其与 template 对象保持一致
|
||||
* 1. 如果 template 中存在 target 中缺少的 key,则添加('[to be translated]')
|
||||
* 2. 如果 target 中存在 template 中不存在的 key,则删除
|
||||
* 3. 对于子对象,递归同步
|
||||
*
|
||||
* @param target 目标对象(需要更新的语言对象)
|
||||
* @param template 主模板对象(中文)
|
||||
* @returns 返回是否对 target 进行了更新
|
||||
*/
|
||||
function syncRecursively(target, template) {
|
||||
var isUpdated = false
|
||||
// 添加 template 中存在但 target 中缺少的 key
|
||||
for (var key in template) {
|
||||
if (!(key in target)) {
|
||||
target[key] =
|
||||
typeof template[key] === 'object' && template[key] !== null ? {} : '[to be translated]:'.concat(template[key])
|
||||
console.log('\u6DFB\u52A0\u65B0\u5C5E\u6027\uFF1A'.concat(key))
|
||||
isUpdated = true
|
||||
}
|
||||
if (typeof template[key] === 'object' && template[key] !== null) {
|
||||
if (typeof target[key] !== 'object' || target[key] === null) {
|
||||
target[key] = {}
|
||||
isUpdated = true
|
||||
}
|
||||
// 递归同步子对象
|
||||
var childUpdated = syncRecursively(target[key], template[key])
|
||||
if (childUpdated) {
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// 删除 target 中存在但 template 中没有的 key
|
||||
for (var targetKey in target) {
|
||||
if (!(targetKey in template)) {
|
||||
console.log('\u79FB\u9664\u591A\u4F59\u5C5E\u6027\uFF1A'.concat(targetKey))
|
||||
delete target[targetKey]
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
return isUpdated
|
||||
}
|
||||
function syncTranslations() {
|
||||
if (!fs.existsSync(baseFilePath)) {
|
||||
console.error(
|
||||
'\u4E3B\u6A21\u677F\u6587\u4EF6 '.concat(
|
||||
baseFileName,
|
||||
' \u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u6216\u6587\u4EF6\u540D\u3002'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
var baseContent = fs.readFileSync(baseFilePath, 'utf-8')
|
||||
var baseJson = {}
|
||||
try {
|
||||
baseJson = JSON.parse(baseContent)
|
||||
} catch (error) {
|
||||
console.error('\u89E3\u6790 '.concat(baseFileName, ' \u51FA\u9519:'), error)
|
||||
return
|
||||
}
|
||||
var files = fs.readdirSync(translationsDir).filter(function (file) {
|
||||
return file.endsWith('.json') && file !== baseFileName
|
||||
})
|
||||
for (var _i = 0, files_1 = files; _i < files_1.length; _i++) {
|
||||
var file = files_1[_i]
|
||||
var filePath = path.join(translationsDir, file)
|
||||
var targetJson = {}
|
||||
try {
|
||||
var fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
targetJson = JSON.parse(fileContent)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'\u89E3\u6790 '.concat(
|
||||
file,
|
||||
' \u51FA\u9519\uFF0C\u8DF3\u8FC7\u6B64\u6587\u4EF6\u3002\u9519\u8BEF\u4FE1\u606F:'
|
||||
),
|
||||
error
|
||||
)
|
||||
continue
|
||||
}
|
||||
var isUpdated = syncRecursively(targetJson, baseJson)
|
||||
if (isUpdated) {
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2), 'utf-8')
|
||||
console.log(
|
||||
'\u6587\u4EF6 '.concat(file, ' \u5DF2\u66F4\u65B0\u540C\u6B65\u4E3B\u6A21\u677F\u7684\u5185\u5BB9\u3002')
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('\u5199\u5165 '.concat(file, ' \u51FA\u9519:'), error)
|
||||
}
|
||||
} else {
|
||||
console.log('\u6587\u4EF6 '.concat(file, ' \u65E0\u9700\u66F4\u65B0\u3002'))
|
||||
}
|
||||
}
|
||||
}
|
||||
syncTranslations()
|
||||
98
scripts/check-i18n.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
const baseLocale = 'zh-CN'
|
||||
const baseFileName = `${baseLocale}.json`
|
||||
const baseFilePath = path.join(translationsDir, baseFileName)
|
||||
|
||||
/**
|
||||
* 递归同步 target 对象,使其与 template 对象保持一致
|
||||
* 1. 如果 template 中存在 target 中缺少的 key,则添加('[to be translated]')
|
||||
* 2. 如果 target 中存在 template 中不存在的 key,则删除
|
||||
* 3. 对于子对象,递归同步
|
||||
*
|
||||
* @param target 目标对象(需要更新的语言对象)
|
||||
* @param template 主模板对象(中文)
|
||||
* @returns 返回是否对 target 进行了更新
|
||||
*/
|
||||
function syncRecursively(target: any, template: any): boolean {
|
||||
let isUpdated = false
|
||||
|
||||
// 添加 template 中存在但 target 中缺少的 key
|
||||
for (const key in template) {
|
||||
if (!(key in target)) {
|
||||
target[key] =
|
||||
typeof template[key] === 'object' && template[key] !== null ? {} : `[to be translated]:${template[key]}`
|
||||
console.log(`添加新属性:${key}`)
|
||||
isUpdated = true
|
||||
}
|
||||
if (typeof template[key] === 'object' && template[key] !== null) {
|
||||
if (typeof target[key] !== 'object' || target[key] === null) {
|
||||
target[key] = {}
|
||||
isUpdated = true
|
||||
}
|
||||
// 递归同步子对象
|
||||
const childUpdated = syncRecursively(target[key], template[key])
|
||||
if (childUpdated) {
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 target 中存在但 template 中没有的 key
|
||||
for (const targetKey in target) {
|
||||
if (!(targetKey in template)) {
|
||||
console.log(`移除多余属性:${targetKey}`)
|
||||
delete target[targetKey]
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
return isUpdated
|
||||
}
|
||||
|
||||
function syncTranslations() {
|
||||
if (!fs.existsSync(baseFilePath)) {
|
||||
console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
|
||||
return
|
||||
}
|
||||
|
||||
const baseContent = fs.readFileSync(baseFilePath, 'utf-8')
|
||||
let baseJson: Record<string, any> = {}
|
||||
try {
|
||||
baseJson = JSON.parse(baseContent)
|
||||
} catch (error) {
|
||||
console.error(`解析 ${baseFileName} 出错:`, error)
|
||||
return
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(translationsDir).filter((file) => file.endsWith('.json') && file !== baseFileName)
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(translationsDir, file)
|
||||
let targetJson: Record<string, any> = {}
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
targetJson = JSON.parse(fileContent)
|
||||
} catch (error) {
|
||||
console.error(`解析 ${file} 出错,跳过此文件。错误信息:`, error)
|
||||
continue
|
||||
}
|
||||
|
||||
const isUpdated = syncRecursively(targetJson, baseJson)
|
||||
|
||||
if (isUpdated) {
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2) + '\n', 'utf-8')
|
||||
console.log(`文件 ${file} 已更新同步主模板的内容`)
|
||||
} catch (error) {
|
||||
console.error(`写入 ${file} 出错:`, error)
|
||||
}
|
||||
} else {
|
||||
console.log(`文件 ${file} 无需更新`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncTranslations()
|
||||
@@ -7,10 +7,10 @@ const config = {
|
||||
VERSION_DB: 'versions.json',
|
||||
LOG_FILE: 'logs.json',
|
||||
MAX_LOGS: 1000 // 最多保存多少条日志
|
||||
};
|
||||
}
|
||||
|
||||
// Worker 入口函数
|
||||
const worker = {
|
||||
// Worker 入口函数
|
||||
const worker = {
|
||||
// 定时器触发配置
|
||||
scheduled: {
|
||||
cron: '*/1 * * * *' // 每分钟执行一次
|
||||
@@ -19,61 +19,67 @@ const config = {
|
||||
// 定时器执行函数 - 只负责检查和更新
|
||||
async scheduled(event, env, ctx) {
|
||||
try {
|
||||
await initDataFiles(env);
|
||||
console.log('开始定时检查新版本...');
|
||||
// 注意这里使用新的函数
|
||||
await checkNewRelease(env);
|
||||
await initDataFiles(env)
|
||||
console.log('开始定时检查新版本...')
|
||||
// 使用新的 checkNewRelease 函数
|
||||
await checkNewRelease(env)
|
||||
} catch (error) {
|
||||
console.error('定时任务执行失败:', error);
|
||||
console.error('定时任务执行失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// HTTP 请求处理函数 - 只负责返回数据
|
||||
async fetch(request, env, ctx) {
|
||||
if (!env || !env.R2_BUCKET) {
|
||||
return new Response(JSON.stringify({
|
||||
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);
|
||||
const url = new URL(request.url)
|
||||
const filename = url.pathname.slice(1)
|
||||
|
||||
try {
|
||||
// 处理文件下载请求
|
||||
if (filename) {
|
||||
return await handleDownload(env, filename);
|
||||
return await handleDownload(env, filename)
|
||||
}
|
||||
|
||||
// 只返回缓存的版本信息
|
||||
return await getCachedRelease(env);
|
||||
return await getCachedRelease(env)
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
}), {
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default worker;
|
||||
export default worker
|
||||
|
||||
/**
|
||||
/**
|
||||
* 添加日志记录函数
|
||||
*/
|
||||
async function addLog(env, type, event, details = null) {
|
||||
async function addLog(env, type, event, details = null) {
|
||||
try {
|
||||
const logFile = await env.R2_BUCKET.get(config.LOG_FILE);
|
||||
let logs = { logs: [] };
|
||||
const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
|
||||
let logs = { logs: [] }
|
||||
|
||||
if (logFile) {
|
||||
logs = JSON.parse(await logFile.text());
|
||||
logs = JSON.parse(await logFile.text())
|
||||
}
|
||||
|
||||
logs.logs.unshift({
|
||||
@@ -81,376 +87,249 @@ const config = {
|
||||
type,
|
||||
event,
|
||||
details
|
||||
});
|
||||
})
|
||||
|
||||
// 保持日志数量在限制内
|
||||
if (logs.logs.length > config.MAX_LOGS) {
|
||||
logs.logs = logs.logs.slice(0, 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));
|
||||
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2))
|
||||
} catch (error) {
|
||||
console.error('写入日志失败:', error);
|
||||
}
|
||||
console.error('写入日志失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并更新发布版本
|
||||
* 由定时器触发,检查新版本并更新 R2 存储
|
||||
*/
|
||||
async function checkAndUpdateRelease(env) {
|
||||
try {
|
||||
// 获取版本数据库
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
let versions = { versions: {}, latestVersion: null, lastChecked: null };
|
||||
|
||||
if (versionDB) {
|
||||
versions = JSON.parse(await versionDB.text());
|
||||
}
|
||||
|
||||
// 获取 GitHub 最新版本
|
||||
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
|
||||
headers: { 'User-Agent': 'CloudflareWorker' },
|
||||
});
|
||||
|
||||
if (!githubResponse.ok) {
|
||||
throw new Error('GitHub API 请求失败');
|
||||
}
|
||||
|
||||
const releaseData = await githubResponse.json();
|
||||
const version = releaseData.tag_name;
|
||||
|
||||
// 更新最后检查时间
|
||||
versions.lastChecked = new Date().toISOString();
|
||||
|
||||
// 检查是否需要更新
|
||||
if (versions.latestVersion !== version) {
|
||||
await addLog(env, 'INFO', `发现新版本: ${version}`);
|
||||
|
||||
// 准备新版本记录
|
||||
const versionRecord = {
|
||||
version,
|
||||
publishedAt: releaseData.published_at,
|
||||
uploadedAt: null,
|
||||
files: releaseData.assets.map(asset => ({
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
uploaded: false
|
||||
})),
|
||||
changelog: releaseData.body
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
for (const asset of releaseData.assets) {
|
||||
try {
|
||||
const existingFile = await env.R2_BUCKET.get(asset.name);
|
||||
if (existingFile) {
|
||||
// 更新文件状态
|
||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
||||
if (fileIndex !== -1) {
|
||||
versionRecord.files[fileIndex].uploaded = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await fetch(asset.browser_download_url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const file = await response.arrayBuffer();
|
||||
await env.R2_BUCKET.put(asset.name, file, {
|
||||
httpMetadata: { contentType: getContentType(asset.name) }
|
||||
});
|
||||
|
||||
// 更新文件状态
|
||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
||||
if (fileIndex !== -1) {
|
||||
versionRecord.files[fileIndex].uploaded = true;
|
||||
}
|
||||
|
||||
await addLog(env, 'INFO', `文件上传成功: ${asset.name}`);
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', `文件上传失败: ${asset.name}`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新版本记录
|
||||
versionRecord.uploadedAt = new Date().toISOString();
|
||||
versions.versions[version] = versionRecord;
|
||||
versions.latestVersion = version;
|
||||
|
||||
// 保存版本数据库
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
||||
|
||||
// 更新缓存
|
||||
const cacheData = {
|
||||
version,
|
||||
publishedAt: releaseData.published_at,
|
||||
changelog: releaseData.body,
|
||||
downloads: versionRecord.files
|
||||
.filter(file => file.uploaded)
|
||||
.map(file => ({
|
||||
name: file.name,
|
||||
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||
size: formatFileSize(file.size)
|
||||
}))
|
||||
};
|
||||
|
||||
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
|
||||
|
||||
// 清理旧版本
|
||||
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a));
|
||||
if (versionList.length > 2) {
|
||||
const oldVersions = versionList.slice(2);
|
||||
for (const oldVersion of oldVersions) {
|
||||
const oldFiles = versions.versions[oldVersion].files;
|
||||
for (const file of oldFiles) {
|
||||
if (file.uploaded) {
|
||||
await env.R2_BUCKET.delete(file.name);
|
||||
await addLog(env, 'INFO', `删除旧文件: ${file.name}`);
|
||||
}
|
||||
}
|
||||
delete versions.versions[oldVersion];
|
||||
}
|
||||
// 保存更新后的版本数据库
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
||||
}
|
||||
|
||||
return cacheData;
|
||||
} else {
|
||||
// 没有新版本,返回缓存数据
|
||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
|
||||
return cached ? JSON.parse(await cached.text()) : null;
|
||||
}
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', '检查更新失败', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 获取最新版本信息
|
||||
*/
|
||||
async function getLatestRelease(env) {
|
||||
async function getLatestRelease(env) {
|
||||
try {
|
||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
|
||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
|
||||
if (!cached) {
|
||||
// 如果缓存不存在,先检查版本数据库
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||
if (versionDB) {
|
||||
const versions = JSON.parse(await versionDB.text());
|
||||
const versions = JSON.parse(await versionDB.text())
|
||||
if (versions.latestVersion) {
|
||||
// 从版本数据库重建缓存
|
||||
const latestVersion = versions.versions[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 => ({
|
||||
.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 env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
|
||||
return new Response(JSON.stringify(cacheData), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
// 如果版本数据库也没有数据,才执行检查更新
|
||||
const data = await checkAndUpdateRelease(env);
|
||||
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();
|
||||
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({
|
||||
await addLog(env, 'ERROR', '获取版本信息失败', error.message)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: '获取版本信息失败: ' + error.message,
|
||||
detail: '请稍后再试'
|
||||
}), {
|
||||
detail: '请稍<EFBFBD><EFBFBD><EFBFBD>再试'
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改下载处理函数,直接接收 env
|
||||
async function handleDownload(env, filename) {
|
||||
// 修改下载处理函数,直接接收 env
|
||||
async function handleDownload(env, filename) {
|
||||
try {
|
||||
const object = await env.R2_BUCKET.get(filename);
|
||||
const object = await env.R2_BUCKET.get(filename)
|
||||
|
||||
if (!object) {
|
||||
return new Response('文件未找到', { status: 404 });
|
||||
return new Response('文件未找到', { status: 404 })
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
const headers = new Headers();
|
||||
object.writeHttpMetadata(headers);
|
||||
headers.set('etag', object.httpEtag);
|
||||
headers.set('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
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 });
|
||||
}
|
||||
console.error('下载文件时发生错误:', error)
|
||||
return new Response('获取文件失败', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 根据文件扩展名获取对应的 Content-Type
|
||||
*/
|
||||
function getContentType(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
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';
|
||||
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;
|
||||
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++;
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 版本号比较函数
|
||||
* 用于对版本号进行排序
|
||||
*/
|
||||
function compareVersions(a, b) {
|
||||
const partsA = a.replace('v', '').split('.');
|
||||
const partsB = b.replace('v', '').split('.');
|
||||
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);
|
||||
const numA = parseInt(partsA[i] || 0)
|
||||
const numB = parseInt(partsB[i] || 0)
|
||||
|
||||
if (numA !== numB) {
|
||||
return numA - numB;
|
||||
return numA - numB
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 初始化数据文件
|
||||
*/
|
||||
async function initDataFiles(env) {
|
||||
async function initDataFiles(env) {
|
||||
try {
|
||||
// 检查并初始化版本数据库
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
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 初始化成功');
|
||||
}
|
||||
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);
|
||||
const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
|
||||
if (!logFile) {
|
||||
const initialLogs = {
|
||||
logs: [{
|
||||
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 初始化成功');
|
||||
}
|
||||
]
|
||||
}
|
||||
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2))
|
||||
console.log('logs.json 初始化成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化数据文件失败:', error);
|
||||
}
|
||||
console.error('初始化数据文件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:只获取缓存的版本信息
|
||||
async function getCachedRelease(env) {
|
||||
// 新增:只获取缓存的版本信息
|
||||
async function getCachedRelease(env) {
|
||||
try {
|
||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
|
||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
|
||||
if (!cached) {
|
||||
// 如果缓存不存在,从版本数据库获取
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||
if (versionDB) {
|
||||
const versions = JSON.parse(await versionDB.text());
|
||||
const versions = JSON.parse(await versionDB.text())
|
||||
if (versions.latestVersion) {
|
||||
const latestVersion = versions.versions[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 => ({
|
||||
.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 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({
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: '没有可用的版本信息'
|
||||
}), {
|
||||
}),
|
||||
{
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 返回缓存数据
|
||||
@@ -459,99 +338,104 @@ const config = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message);
|
||||
throw error;
|
||||
}
|
||||
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:只检查新版本并更新
|
||||
async function checkNewRelease(env) {
|
||||
// 新增:只检查新版本并更新
|
||||
async function checkNewRelease(env) {
|
||||
try {
|
||||
// 获取 GitHub 最新版本
|
||||
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
|
||||
headers: { 'User-Agent': 'CloudflareWorker' },
|
||||
});
|
||||
headers: { 'User-Agent': 'CloudflareWorker' }
|
||||
})
|
||||
|
||||
if (!githubResponse.ok) {
|
||||
throw new Error('GitHub API 请求失败');
|
||||
throw new Error('GitHub API 请求失败')
|
||||
}
|
||||
|
||||
const releaseData = await githubResponse.json();
|
||||
const version = releaseData.tag_name;
|
||||
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() };
|
||||
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());
|
||||
versions = JSON.parse(await versionDB.text())
|
||||
}
|
||||
|
||||
// 如果版本相同,不需要更新
|
||||
if (versions.latestVersion === version) {
|
||||
console.log('当前已是最新版本');
|
||||
return;
|
||||
// 移除版本检查,改为记录是否有文件更新的标志
|
||||
let hasUpdates = false
|
||||
if (versions.latestVersion !== version) {
|
||||
await addLog(env, 'INFO', `发现新版本: ${version}`)
|
||||
hasUpdates = true
|
||||
} else {
|
||||
await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`)
|
||||
}
|
||||
|
||||
await addLog(env, 'INFO', `发现新版本: ${version}`);
|
||||
|
||||
// 准备新版本记录
|
||||
const versionRecord = {
|
||||
version,
|
||||
publishedAt: releaseData.published_at,
|
||||
uploadedAt: null,
|
||||
files: releaseData.assets.map(asset => ({
|
||||
files: releaseData.assets.map((asset) => ({
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
uploaded: false
|
||||
})),
|
||||
changelog: releaseData.body
|
||||
};
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
// 检查并上传文件
|
||||
for (const asset of releaseData.assets) {
|
||||
try {
|
||||
const existingFile = await env.R2_BUCKET.get(asset.name);
|
||||
if (existingFile) {
|
||||
// 更新文件状态
|
||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
||||
if (fileIndex !== -1) {
|
||||
versionRecord.files[fileIndex].uploaded = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await fetch(asset.browser_download_url);
|
||||
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}`);
|
||||
throw new Error(`下载失败: HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const file = await response.arrayBuffer();
|
||||
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);
|
||||
const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name)
|
||||
if (fileIndex !== -1) {
|
||||
versionRecord.files[fileIndex].uploaded = true;
|
||||
versionRecord.files[fileIndex].uploaded = true
|
||||
}
|
||||
|
||||
await addLog(env, 'INFO', `文件上传成功: ${asset.name}`);
|
||||
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);
|
||||
await addLog(env, 'ERROR', `文件处理失败: ${asset.name}`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在有更新或是新版本时才更新数据库和缓存
|
||||
if (hasUpdates) {
|
||||
// 更新版本记录
|
||||
versionRecord.uploadedAt = new Date().toISOString();
|
||||
versions.versions[version] = versionRecord;
|
||||
versions.latestVersion = version;
|
||||
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));
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2))
|
||||
|
||||
// 更新缓存
|
||||
const cacheData = {
|
||||
@@ -559,37 +443,87 @@ const config = {
|
||||
publishedAt: releaseData.published_at,
|
||||
changelog: releaseData.body,
|
||||
downloads: versionRecord.files
|
||||
.filter(file => file.uploaded)
|
||||
.map(file => ({
|
||||
.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 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));
|
||||
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a))
|
||||
if (versionList.length > 2) {
|
||||
const oldVersions = versionList.slice(2);
|
||||
for (const oldVersion of oldVersions) {
|
||||
const oldFiles = versions.versions[oldVersion].files;
|
||||
for (const file of oldFiles) {
|
||||
if (file.uploaded) {
|
||||
await env.R2_BUCKET.delete(file.name);
|
||||
await addLog(env, 'INFO', `删除旧文件: ${file.name}`);
|
||||
}
|
||||
}
|
||||
delete versions.versions[oldVersion];
|
||||
}
|
||||
// 保存更新后的版本数据库
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
||||
// 获取需要保留的两个最新版本
|
||||
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))
|
||||
}
|
||||
|
||||
return cacheData;
|
||||
// 删除所有旧版本文件
|
||||
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', '检查新版本失败', error.message);
|
||||
throw 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,95 +1,3 @@
|
||||
export const isMac = process.platform === 'darwin'
|
||||
export const isWin = process.platform === 'win32'
|
||||
export const isLinux = process.platform === 'linux'
|
||||
|
||||
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 代码文件
|
||||
]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { app } 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'
|
||||
@@ -16,6 +16,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// 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()
|
||||
|
||||
@@ -26,16 +27,15 @@ if (!app.requestSingleInstanceLock()) {
|
||||
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) {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
windowService.createMainWindow()
|
||||
} else {
|
||||
windowService.showMainWindow()
|
||||
}
|
||||
})
|
||||
|
||||
registerZoomShortcut(mainWindow)
|
||||
registerShortcuts(mainWindow)
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
@@ -48,12 +48,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
|
||||
// Listen for second instance
|
||||
app.on('second-instance', () => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0]
|
||||
if (mainWindow) {
|
||||
mainWindow.isMinimized() && mainWindow.restore()
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
windowService.showMainWindow()
|
||||
})
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
@@ -10,8 +10,16 @@ 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 { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { getResourcePath } from './utils'
|
||||
import { decrypt, encrypt } from './utils/aes'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
@@ -27,14 +35,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
appPath: app.getAppPath(),
|
||||
filesPath: path.join(app.getPath('userData'), 'Data', 'Files'),
|
||||
appDataPath: app.getPath('userData'),
|
||||
resourcesPath: getResourcePath(),
|
||||
logsPath: log.transports.file.getFile().path
|
||||
}))
|
||||
|
||||
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 } : { mode: 'direct' }
|
||||
await Promise.all(sessions.map((session) => session.setProxy(proxyConfig)))
|
||||
proxy === 'system' ? { mode: 'system' } : proxy ? { mode: 'custom', url: proxy } : { mode: 'none' }
|
||||
await proxyManager.configureProxy(proxyConfig)
|
||||
})
|
||||
|
||||
ipcMain.handle('app:reload', () => mainWindow.reload())
|
||||
@@ -50,6 +58,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
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: ThemeMode) => {
|
||||
configManager.setTheme(theme)
|
||||
@@ -81,9 +99,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// 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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -99,6 +118,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)
|
||||
@@ -113,6 +133,10 @@ 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) => {
|
||||
@@ -133,4 +157,54 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
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())
|
||||
|
||||
// aes
|
||||
ipcMain.handle('aes:encrypt', (_, text: string, secretKey: string, iv: string) => encrypt(text, secretKey, iv))
|
||||
ipcMain.handle('aes:decrypt', (_, encryptedData: string, iv: string, secretKey: string) =>
|
||||
decrypt(encryptedData, iv, secretKey)
|
||||
)
|
||||
}
|
||||
|
||||
22
src/main/loader/draftsExportLoader.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
import { JsonLoader } from '@llm-tools/embedjs'
|
||||
|
||||
/**
|
||||
* Drafts 应用导出的笔记文件加载器
|
||||
* 原始文件是一个 JSON 数组。每条笔记只保留 content、tags、modified_at 三个字段
|
||||
*/
|
||||
export class DraftsExportLoader extends JsonLoader {
|
||||
constructor(filePath: string) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const rawJson = JSON.parse(fileContent) as any[]
|
||||
const json = rawJson.map((item) => {
|
||||
return {
|
||||
content: item.content?.replace(/\n/g, '<br>'),
|
||||
tags: item.tags,
|
||||
modified_at: item.created_at
|
||||
}
|
||||
})
|
||||
super({ object: json })
|
||||
}
|
||||
}
|
||||
228
src/main/loader/epubLoader.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
|
||||
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
|
||||
import { cleanString } from '@llm-tools/embedjs-utils'
|
||||
import Logger from 'electron-log'
|
||||
import EPub from 'epub'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* epub 加载器的配置选项
|
||||
*/
|
||||
interface EpubLoaderOptions {
|
||||
/** epub 文件路径 */
|
||||
filePath: string
|
||||
/** 文本分块大小 */
|
||||
chunkSize: number
|
||||
/** 分块重叠大小 */
|
||||
chunkOverlap: number
|
||||
}
|
||||
|
||||
/**
|
||||
* epub 文件的元数据信息
|
||||
*/
|
||||
interface EpubMetadata {
|
||||
/** 作者显示名称(例如:"Lewis Carroll") */
|
||||
creator?: string
|
||||
/** 作者规范化名称,用于排序和索引(例如:"Carroll, Lewis") */
|
||||
creatorFileAs?: string
|
||||
/** 书籍标题(例如:"Alice's Adventures in Wonderland") */
|
||||
title?: string
|
||||
/** 语言代码(例如:"en" 或 "zh-CN") */
|
||||
language?: string
|
||||
/** 主题或分类(例如:"Fantasy"、"Fiction") */
|
||||
subject?: string
|
||||
/** 创建日期(例如:"2024-02-14") */
|
||||
date?: string
|
||||
/** 书籍描述或简介 */
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* epub 章节信息
|
||||
*/
|
||||
interface EpubChapter {
|
||||
/** 章节 ID */
|
||||
id: string
|
||||
/** 章节标题 */
|
||||
title?: string
|
||||
/** 章节顺序 */
|
||||
order?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* epub 文件加载器
|
||||
* 用于解析 epub 电子书文件,提取文本内容和元数据
|
||||
*/
|
||||
export class EpubLoader extends BaseLoader<Record<string, string | number | boolean>, Record<string, unknown>> {
|
||||
protected filePath: string
|
||||
protected chunkSize: number
|
||||
protected chunkOverlap: number
|
||||
private extractedText: string
|
||||
private metadata: EpubMetadata | null
|
||||
|
||||
/**
|
||||
* 创建 epub 加载器实例
|
||||
* @param options 加载器配置选项
|
||||
*/
|
||||
constructor(options: EpubLoaderOptions) {
|
||||
super(options.filePath, {
|
||||
chunkSize: options.chunkSize,
|
||||
chunkOverlap: options.chunkOverlap
|
||||
})
|
||||
this.filePath = options.filePath
|
||||
this.chunkSize = options.chunkSize
|
||||
this.chunkOverlap = options.chunkOverlap
|
||||
this.extractedText = ''
|
||||
this.metadata = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 epub 文件初始化完成
|
||||
* epub 库使用事件机制,需要等待 'end' 事件触发后才能访问文件内容
|
||||
* @param epub epub 实例
|
||||
* @returns 元数据和章节信息
|
||||
*/
|
||||
private waitForEpubInit(epub: any): Promise<{ metadata: EpubMetadata; chapters: EpubChapter[] }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
epub.on('end', () => {
|
||||
// 提取元数据
|
||||
const metadata: EpubMetadata = {
|
||||
creator: epub.metadata.creator,
|
||||
creatorFileAs: epub.metadata.creatorFileAs,
|
||||
title: epub.metadata.title,
|
||||
language: epub.metadata.language,
|
||||
subject: epub.metadata.subject,
|
||||
date: epub.metadata.date,
|
||||
description: epub.metadata.description
|
||||
}
|
||||
|
||||
// 提取章节信息
|
||||
const chapters: EpubChapter[] = epub.flow.map((chapter: any, index: number) => ({
|
||||
id: chapter.id,
|
||||
title: chapter.title || `Chapter ${index + 1}`,
|
||||
order: index + 1
|
||||
}))
|
||||
|
||||
resolve({ metadata, chapters })
|
||||
})
|
||||
|
||||
epub.on('error', (error: Error) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
epub.parse()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取章节内容
|
||||
* @param epub epub 实例
|
||||
* @param chapterId 章节 ID
|
||||
* @returns 章节文本内容
|
||||
*/
|
||||
private getChapter(epub: any, chapterId: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
epub.getChapter(chapterId, (error: Error | null, text: string) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(text)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 epub 文件中提取文本内容
|
||||
* 1. 检查文件是否存在
|
||||
* 2. 初始化 epub 并获取元数据
|
||||
* 3. 遍历所有章节并提取文本
|
||||
* 4. 清理 HTML 标签
|
||||
* 5. 合并所有章节文本
|
||||
*/
|
||||
private async extractTextFromEpub() {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(this.filePath)) {
|
||||
throw new Error(`File not found: ${this.filePath}`)
|
||||
}
|
||||
|
||||
const epub = new EPub(this.filePath)
|
||||
|
||||
// 等待 epub 初始化完成并获取元数据
|
||||
const { metadata, chapters } = await this.waitForEpubInit(epub)
|
||||
this.metadata = metadata
|
||||
|
||||
if (!epub.flow || epub.flow.length === 0) {
|
||||
throw new Error('No content found in epub file')
|
||||
}
|
||||
|
||||
const chapterTexts: string[] = []
|
||||
|
||||
// 遍历所有章节
|
||||
for (const chapter of chapters) {
|
||||
try {
|
||||
const content = await this.getChapter(epub, chapter.id)
|
||||
|
||||
if (!content) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 移除 HTML 标签并清理文本
|
||||
const text = content
|
||||
.replace(/<[^>]*>/g, ' ') // 移除所有 HTML 标签
|
||||
.replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格
|
||||
.trim() // 移除首尾空白
|
||||
|
||||
if (text) {
|
||||
chapterTexts.push(text)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[EpubLoader] Error processing chapter ${chapter.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用双换行符连接所有章节文本
|
||||
this.extractedText = chapterTexts.join('\n\n')
|
||||
} catch (error) {
|
||||
Logger.error('[EpubLoader] Error in extractTextFromEpub:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文本块
|
||||
* 重写 BaseLoader 的方法,将提取的文本分割成适当大小的块
|
||||
* 每个块都包含源文件和元数据信息
|
||||
*/
|
||||
override async *getUnfilteredChunks() {
|
||||
// 如果还没有提取文本,先提取
|
||||
if (!this.extractedText) {
|
||||
await this.extractTextFromEpub()
|
||||
}
|
||||
|
||||
Logger.info('[EpubLoader] 书名:', this.metadata?.title || '未知书名', ' 文本大小:', this.extractedText.length)
|
||||
|
||||
// 创建文本分块器
|
||||
const chunker = new RecursiveCharacterTextSplitter({
|
||||
chunkSize: this.chunkSize,
|
||||
chunkOverlap: this.chunkOverlap
|
||||
})
|
||||
|
||||
// 清理并分割文本
|
||||
const chunks = await chunker.splitText(cleanString(this.extractedText))
|
||||
|
||||
// 为每个文本块添加元数据
|
||||
for (const chunk of chunks) {
|
||||
yield {
|
||||
pageContent: chunk,
|
||||
metadata: {
|
||||
source: this.filePath,
|
||||
title: this.metadata?.title || '',
|
||||
creator: this.metadata?.creator || '',
|
||||
language: this.metadata?.language || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/main/loader/index.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
||||
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||
import { LoaderReturn } from '@shared/config/types'
|
||||
import { FileType, KnowledgeBaseParams } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { DraftsExportLoader } from './draftsExportLoader'
|
||||
import { EpubLoader } from './epubLoader'
|
||||
import { OdLoader, OdType } from './odLoader'
|
||||
|
||||
// embedjs内置loader类型
|
||||
const commonExts = ['.pdf', '.csv', '.docx', '.pptx', '.xlsx', '.md']
|
||||
|
||||
export async function addOdLoader(
|
||||
ragApplication: RAGApplication,
|
||||
file: FileType,
|
||||
base: KnowledgeBaseParams,
|
||||
forceReload: boolean
|
||||
): Promise<AddLoaderReturn> {
|
||||
const loaderMap: Record<string, OdType> = {
|
||||
'.odt': OdType.OdtLoader,
|
||||
'.ods': OdType.OdsLoader,
|
||||
'.odp': OdType.OdpLoader
|
||||
}
|
||||
const odType = loaderMap[file.ext]
|
||||
if (!odType) {
|
||||
throw new Error('Unknown odType')
|
||||
}
|
||||
return ragApplication.addLoader(
|
||||
new OdLoader({
|
||||
odType,
|
||||
filePath: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
export async function addFileLoader(
|
||||
ragApplication: RAGApplication,
|
||||
file: FileType,
|
||||
base: KnowledgeBaseParams,
|
||||
forceReload: boolean
|
||||
): Promise<LoaderReturn> {
|
||||
// 内置类型
|
||||
if (commonExts.includes(file.ext)) {
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
// @ts-ignore LocalPathLoader
|
||||
new LocalPathLoader({ path: file.path, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
} as LoaderReturn
|
||||
}
|
||||
|
||||
// 自定义类型
|
||||
if (['.odt', '.ods', '.odp'].includes(file.ext)) {
|
||||
const loaderReturn = await addOdLoader(ragApplication, file, base, forceReload)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
} as LoaderReturn
|
||||
}
|
||||
|
||||
// epub 文件处理
|
||||
if (file.ext === '.epub') {
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
new EpubLoader({
|
||||
filePath: file.path,
|
||||
chunkSize: base.chunkSize ?? 1000,
|
||||
chunkOverlap: base.chunkOverlap ?? 200
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
} as LoaderReturn
|
||||
}
|
||||
|
||||
// DraftsExport类型 (file.ext会自动转换成小写)
|
||||
if (['.draftsexport'].includes(file.ext)) {
|
||||
const loaderReturn = await ragApplication.addLoader(new DraftsExportLoader(file.path) as any, forceReload)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
}
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(file.path, 'utf-8')
|
||||
|
||||
// HTML类型
|
||||
if (['.html', '.htm'].includes(file.ext)) {
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
new WebLoader({
|
||||
urlOrContent: fileContent,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
}
|
||||
}
|
||||
|
||||
// JSON类型
|
||||
if (['.json'].includes(file.ext)) {
|
||||
let jsonObject = {}
|
||||
let jsonParsed = true
|
||||
try {
|
||||
jsonObject = JSON.parse(fileContent)
|
||||
} catch (error) {
|
||||
jsonParsed = false
|
||||
Logger.warn('[KnowledgeBase] failed parsing json file, failling back to text processing:', file.path, error)
|
||||
}
|
||||
if (jsonParsed) {
|
||||
const loaderReturn = await ragApplication.addLoader(new JsonLoader({ object: jsonObject }))
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文本类型
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
|
||||
Logger.info('[KnowledgeBase] processing file', file.path)
|
||||
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
} as LoaderReturn
|
||||
}
|
||||
71
src/main/loader/odLoader.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
|
||||
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
|
||||
import { cleanString } from '@llm-tools/embedjs-utils'
|
||||
import md5 from 'md5'
|
||||
import { OfficeParserConfig, parseOfficeAsync } from 'officeparser'
|
||||
|
||||
export enum OdType {
|
||||
OdtLoader = 'OdtLoader',
|
||||
OdsLoader = 'OdsLoader',
|
||||
OdpLoader = 'OdpLoader',
|
||||
undefined = 'undefined'
|
||||
}
|
||||
|
||||
export class OdLoader<OdType> extends BaseLoader<{ type: string }> {
|
||||
private readonly odType: OdType
|
||||
private readonly filePath: string
|
||||
private extractedText: string
|
||||
private config: OfficeParserConfig
|
||||
|
||||
constructor({
|
||||
odType,
|
||||
filePath,
|
||||
chunkSize,
|
||||
chunkOverlap
|
||||
}: {
|
||||
odType: OdType
|
||||
filePath: string
|
||||
chunkSize?: number
|
||||
chunkOverlap?: number
|
||||
}) {
|
||||
super(`${odType}_${md5(filePath)}`, { filePath }, chunkSize ?? 1000, chunkOverlap ?? 0)
|
||||
this.odType = odType
|
||||
this.filePath = filePath
|
||||
this.extractedText = ''
|
||||
this.config = {
|
||||
newlineDelimiter: ' ',
|
||||
ignoreNotes: false
|
||||
}
|
||||
}
|
||||
|
||||
private async extractTextFromOdt() {
|
||||
try {
|
||||
this.extractedText = await parseOfficeAsync(this.filePath, this.config)
|
||||
} catch (err) {
|
||||
console.error('odLoader error', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
override async *getUnfilteredChunks() {
|
||||
if (!this.extractedText) {
|
||||
await this.extractTextFromOdt()
|
||||
}
|
||||
const chunker = new RecursiveCharacterTextSplitter({
|
||||
chunkSize: this.chunkSize,
|
||||
chunkOverlap: this.chunkOverlap
|
||||
})
|
||||
|
||||
const chunks = await chunker.splitText(cleanString(this.extractedText))
|
||||
|
||||
for (const chunk of chunks) {
|
||||
yield {
|
||||
pageContent: chunk,
|
||||
metadata: {
|
||||
type: this.odType as string,
|
||||
source: this.filePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,16 @@ 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.forceDevUpdateConfig = !app.isPackaged
|
||||
autoUpdater.autoDownload = true
|
||||
|
||||
// 检测下载错误
|
||||
@@ -18,7 +21,7 @@ export default class AppUpdater {
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||
autoUpdater.logger?.info('检测到新版本,开始自动下载')
|
||||
logger.info('检测到新版本', releaseInfo)
|
||||
mainWindow.webContents.send('update-available', releaseInfo)
|
||||
})
|
||||
|
||||
@@ -29,18 +32,22 @@ 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
|
||||
@@ -49,10 +56,29 @@ export default class AppUpdater {
|
||||
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,5 +1,6 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { exec } from 'child_process'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs-extra'
|
||||
@@ -18,6 +19,53 @@ class BackupManager {
|
||||
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
||||
}
|
||||
|
||||
private async setWritableRecursive(dirPath: string): Promise<void> {
|
||||
try {
|
||||
const items = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dirPath, item.name)
|
||||
|
||||
// 先处理子目录
|
||||
if (item.isDirectory()) {
|
||||
await this.setWritableRecursive(fullPath)
|
||||
}
|
||||
|
||||
// 统一设置权限(Windows需要特殊处理)
|
||||
await this.forceSetWritable(fullPath)
|
||||
}
|
||||
|
||||
// 确保根目录权限
|
||||
await this.forceSetWritable(dirPath)
|
||||
} catch (error) {
|
||||
Logger.error(`权限设置失败:${dirPath}`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 新增跨平台权限设置方法
|
||||
private async forceSetWritable(targetPath: string): Promise<void> {
|
||||
try {
|
||||
// Windows系统需要先取消只读属性
|
||||
if (process.platform === 'win32') {
|
||||
await fs.chmod(targetPath, 0o666) // Windows会忽略权限位但能移除只读
|
||||
} else {
|
||||
const stats = await fs.stat(targetPath)
|
||||
const mode = stats.isDirectory() ? 0o777 : 0o666
|
||||
await fs.chmod(targetPath, mode)
|
||||
}
|
||||
|
||||
// 双重保险:使用文件属性命令(Windows专用)
|
||||
if (process.platform === 'win32') {
|
||||
await exec(`attrib -R "${targetPath}" /L /D`)
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
Logger.warn(`权限设置警告:${targetPath}`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async backup(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
fileName: string,
|
||||
@@ -35,6 +83,7 @@ class BackupManager {
|
||||
const sourcePath = path.join(app.getPath('userData'), 'Data')
|
||||
const tempDataDir = path.join(this.tempDir, 'Data')
|
||||
await fs.copy(sourcePath, tempDataDir)
|
||||
await this.setWritableRecursive(tempDataDir)
|
||||
|
||||
// 使用 adm-zip 创建压缩文件
|
||||
const zip = new AdmZip()
|
||||
@@ -75,12 +124,14 @@ class BackupManager {
|
||||
// 恢复 Data 目录
|
||||
const sourcePath = path.join(this.tempDir, 'Data')
|
||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||
await this.setWritableRecursive(destPath)
|
||||
await fs.remove(destPath)
|
||||
await fs.copy(sourcePath, destPath)
|
||||
|
||||
Logger.log('[backup] step 4: clean up temp directory')
|
||||
|
||||
// 清理临时目录
|
||||
await this.setWritableRecursive(this.tempDir)
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('[backup] step 5: Restore completed successfully')
|
||||
|
||||
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,4 +1,5 @@
|
||||
import { LanguageVarious, ThemeMode } from '@types'
|
||||
import { ZOOM_SHORTCUTS } from '@shared/config/constant'
|
||||
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
|
||||
import { app } from 'electron'
|
||||
import Store from 'electron-store'
|
||||
|
||||
@@ -29,7 +30,7 @@ export class ConfigManager {
|
||||
this.store.set('theme', theme)
|
||||
}
|
||||
|
||||
isTray(): boolean {
|
||||
getTray(): boolean {
|
||||
return !!this.store.get('tray', true)
|
||||
}
|
||||
|
||||
@@ -70,6 +71,42 @@ export class ConfigManager {
|
||||
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,6 +263,13 @@ 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()
|
||||
@@ -298,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,
|
||||
@@ -376,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
|
||||
}
|
||||
|
||||
68
src/main/services/GeminiService.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { FileMetadataResponse, FileState, GoogleAIFileManager } from '@google/generative-ai/server'
|
||||
import { FileType } from '@types'
|
||||
import fs from 'fs'
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
import { proxyManager } from './ProxyManager'
|
||||
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) {
|
||||
setGlobalDispatcher(new ProxyAgent(proxyManager.getProxyUrl() || ''))
|
||||
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> {
|
||||
setGlobalDispatcher(new ProxyAgent(proxyManager.getProxyUrl() || ''))
|
||||
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) {
|
||||
setGlobalDispatcher(new ProxyAgent(proxyManager.getProxyUrl() || ''))
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
return await fileManager.listFiles()
|
||||
}
|
||||
|
||||
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
|
||||
setGlobalDispatcher(new ProxyAgent(proxyManager.getProxyUrl() || ''))
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
await fileManager.deleteFile(fileId)
|
||||
}
|
||||
}
|
||||
202
src/main/services/KnowledgeService.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import * as fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { RAGApplication, RAGApplicationBuilder, TextLoader } from '@llm-tools/embedjs'
|
||||
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { LibSqlDb } from '@llm-tools/embedjs-libsql'
|
||||
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 { addFileLoader } from '@main/loader'
|
||||
import { getInstanceName } from '@main/utils'
|
||||
import { getAllFiles } from '@main/utils/file'
|
||||
import type { LoaderReturn } from '@shared/config/types'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
|
||||
import { app } from 'electron'
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { proxyManager } from './ProxyManager'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
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> => {
|
||||
const batchSize = 10
|
||||
return new RAGApplicationBuilder()
|
||||
.setModel('NO_MODEL')
|
||||
.setEmbeddingModel(
|
||||
apiVersion
|
||||
? new AzureOpenAiEmbeddings({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
||||
configuration: { httpAgent: proxyManager.getProxyAgent() },
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
: new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
configuration: { baseURL, httpAgent: proxyManager.getProxyAgent() },
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
)
|
||||
.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<LoaderReturn> => {
|
||||
setGlobalDispatcher(new ProxyAgent(proxyManager.getProxyUrl() || ''))
|
||||
const ragApplication = await this.getRagApplication(base)
|
||||
|
||||
const sendDirectoryProcessingPercent = (totalFiles: number, processedFiles: number) => {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
mainWindow?.webContents.send('directory-processing-percent', {
|
||||
itemId: item.id,
|
||||
percent: (processedFiles / totalFiles) * 100
|
||||
})
|
||||
}
|
||||
|
||||
if (item.type === 'directory') {
|
||||
const directory = item.content as string
|
||||
const files = getAllFiles(directory)
|
||||
const totalFiles = files.length
|
||||
let processedFiles = 0
|
||||
|
||||
const loaderPromises = files.map(async (file) => {
|
||||
const result = await addFileLoader(ragApplication, file, base, forceReload)
|
||||
processedFiles++
|
||||
sendDirectoryProcessingPercent(totalFiles, processedFiles)
|
||||
return result
|
||||
})
|
||||
|
||||
const loaderResults = await Promise.allSettled(loaderPromises)
|
||||
// @ts-ignore uniqueId
|
||||
const uniqueIds = loaderResults
|
||||
.filter((result) => result.status === 'fulfilled')
|
||||
.map((result) => result.value.uniqueId)
|
||||
|
||||
return {
|
||||
entriesAdded: loaderResults.length,
|
||||
uniqueId: `DirectoryLoader_${uuidv4()}`,
|
||||
uniqueIds,
|
||||
loaderType: 'DirectoryLoader'
|
||||
} as LoaderReturn
|
||||
}
|
||||
|
||||
if (item.type === 'url') {
|
||||
const content = item.content as string
|
||||
if (content.startsWith('http')) {
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
new WebLoader({ urlOrContent: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
} as LoaderReturn
|
||||
}
|
||||
}
|
||||
|
||||
if (item.type === 'sitemap') {
|
||||
const content = item.content as string
|
||||
// @ts-ignore loader type
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
new SitemapLoader({ url: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
} as LoaderReturn
|
||||
}
|
||||
|
||||
if (item.type === 'note') {
|
||||
const content = item.content as string
|
||||
console.debug('chunkSize', base.chunkSize)
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
new TextLoader({ text: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
|
||||
forceReload
|
||||
)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
} as LoaderReturn
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
const file = item.content as FileType
|
||||
|
||||
return await addFileLoader(ragApplication, file, base, forceReload)
|
||||
}
|
||||
|
||||
return { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' }
|
||||
}
|
||||
|
||||
public remove = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }
|
||||
): Promise<void> => {
|
||||
const ragApplication = await this.getRagApplication(base)
|
||||
console.debug(`[ KnowledgeService Remove Item UniqueId: ${uniqueId}]`)
|
||||
for (const id of uniqueIds) {
|
||||
await ragApplication.deleteLoader(id)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
116
src/main/services/ProxyManager.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { ProxyConfig as _ProxyConfig, session } from 'electron'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
|
||||
type ProxyMode = 'system' | 'custom' | 'none'
|
||||
|
||||
export interface ProxyConfig {
|
||||
mode: ProxyMode
|
||||
url?: string | null
|
||||
}
|
||||
|
||||
export class ProxyManager {
|
||||
private config: ProxyConfig
|
||||
private proxyAgent: HttpsProxyAgent | null = null
|
||||
private proxyUrl: string | null = null
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
mode: 'system',
|
||||
url: ''
|
||||
}
|
||||
}
|
||||
|
||||
private async setSessionsProxy(config: _ProxyConfig): Promise<void> {
|
||||
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
|
||||
await Promise.all(sessions.map((session) => session.setProxy(config)))
|
||||
}
|
||||
|
||||
async configureProxy(config: ProxyConfig): Promise<void> {
|
||||
try {
|
||||
this.config = config
|
||||
if (this.config.mode === 'system') {
|
||||
await this.setSystemProxy()
|
||||
} else if (this.config.mode == 'custom') {
|
||||
await this.setCustomProxy()
|
||||
} else {
|
||||
await this.clearProxy()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to config proxy:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private setEnvironment(url: string): void {
|
||||
process.env.grpc_proxy = url
|
||||
process.env.HTTP_PROXY = url
|
||||
process.env.HTTPS_PROXY = url
|
||||
process.env.http_proxy = url
|
||||
process.env.https_proxy = url
|
||||
}
|
||||
|
||||
private async setSystemProxy(): Promise<void> {
|
||||
try {
|
||||
this.proxyUrl = await this.resolveSystemProxy()
|
||||
if (this.proxyUrl) {
|
||||
this.proxyAgent = new HttpsProxyAgent(this.proxyUrl)
|
||||
this.setEnvironment(this.proxyUrl)
|
||||
await this.setSessionsProxy({ mode: 'system' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set system proxy:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async setCustomProxy(): Promise<void> {
|
||||
try {
|
||||
if (this.config.url) {
|
||||
this.proxyUrl = this.config.url
|
||||
this.proxyAgent = new HttpsProxyAgent(this.config.url)
|
||||
this.setEnvironment(this.config.url)
|
||||
await this.setSessionsProxy({ proxyRules: this.config.url })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set custom proxy:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async clearProxy(): Promise<void> {
|
||||
delete process.env.HTTP_PROXY
|
||||
delete process.env.HTTPS_PROXY
|
||||
await this.setSessionsProxy({})
|
||||
this.config = { mode: 'none' }
|
||||
}
|
||||
|
||||
private async resolveSystemProxy(): Promise<string | null> {
|
||||
try {
|
||||
return await this.resolveElectronProxy()
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve system proxy:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveElectronProxy(): Promise<string | null> {
|
||||
try {
|
||||
const proxyString = await session.defaultSession.resolveProxy('https://dummy.com')
|
||||
const [protocol, address] = proxyString.split(';')[0].split(' ')
|
||||
return protocol === 'PROXY' ? `http://${address}` : null
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve electron proxy:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
getProxyAgent(): HttpsProxyAgent | null {
|
||||
return this.proxyAgent
|
||||
}
|
||||
|
||||
getProxyUrl(): string | null {
|
||||
return this.proxyUrl
|
||||
}
|
||||
}
|
||||
|
||||
export const proxyManager = new ProxyManager()
|
||||
@@ -1,70 +1,215 @@
|
||||
import { Shortcut } from '@types'
|
||||
import { BrowserWindow, globalShortcut } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { configManager } from './ConfigManager'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
export function registerZoomShortcut(mainWindow: BrowserWindow) {
|
||||
// 初始化缩放值
|
||||
const initialZoom = configManager.getZoomFactor()
|
||||
mainWindow.webContents.setZoomFactor(initialZoom)
|
||||
let showAppAccelerator: string | null = null
|
||||
let showMiniWindowAccelerator: string | null = null
|
||||
|
||||
const handleZoom = (delta: number) => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom + delta
|
||||
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()) {
|
||||
if (window.isFocused()) {
|
||||
window.hide()
|
||||
} else {
|
||||
window.focus()
|
||||
}
|
||||
} else {
|
||||
window.show()
|
||||
window.focus()
|
||||
}
|
||||
}
|
||||
case 'mini_window':
|
||||
return () => {
|
||||
windowService.toggleMiniWindow()
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatShortcutKey(shortcut: string[]): string {
|
||||
return shortcut.join('+')
|
||||
}
|
||||
|
||||
function handleZoom(delta: number) {
|
||||
return (window: BrowserWindow) => {
|
||||
const currentZoom = configManager.getZoomFactor()
|
||||
const newZoom = Number((currentZoom + delta).toFixed(1))
|
||||
if (newZoom >= 0.1 && newZoom <= 5.0) {
|
||||
mainWindow.webContents.setZoomFactor(newZoom)
|
||||
window.webContents.setZoomFactor(newZoom)
|
||||
configManager.setZoomFactor(newZoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
|
||||
shortcut: string | string[]
|
||||
): string => {
|
||||
const accelerator = (() => {
|
||||
if (Array.isArray(shortcut)) {
|
||||
return shortcut
|
||||
} else {
|
||||
return shortcut.split('+').map((key) => key.trim())
|
||||
}
|
||||
})()
|
||||
|
||||
const registerShortcuts = () => {
|
||||
// 放大快捷键
|
||||
globalShortcut.register('CommandOrControl+=', () => handleZoom(0.1))
|
||||
globalShortcut.register('CommandOrControl+numadd', () => handleZoom(0.1))
|
||||
|
||||
// 缩小快捷键
|
||||
globalShortcut.register('CommandOrControl+-', () => handleZoom(-0.1))
|
||||
globalShortcut.register('CommandOrControl+numsub', () => handleZoom(-0.1))
|
||||
|
||||
// 重置快捷键
|
||||
globalShortcut.register('CommandOrControl+0', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.setZoomFactor(1)
|
||||
configManager.setZoomFactor(1)
|
||||
return accelerator
|
||||
.map((key) => {
|
||||
switch (key) {
|
||||
case 'Command':
|
||||
return 'CommandOrControl'
|
||||
case 'Control':
|
||||
return 'Control'
|
||||
case 'Ctrl':
|
||||
return 'Control'
|
||||
case 'ArrowUp':
|
||||
return 'Up'
|
||||
case 'ArrowDown':
|
||||
return 'Down'
|
||||
case 'ArrowLeft':
|
||||
return 'Left'
|
||||
case 'ArrowRight':
|
||||
return 'Right'
|
||||
case 'AltGraph':
|
||||
return 'Alt'
|
||||
case 'Slash':
|
||||
return '/'
|
||||
case 'Semicolon':
|
||||
return ';'
|
||||
case 'BracketLeft':
|
||||
return '['
|
||||
case 'BracketRight':
|
||||
return ']'
|
||||
case 'Backslash':
|
||||
return '\\'
|
||||
case 'Quote':
|
||||
return "'"
|
||||
case 'Comma':
|
||||
return ','
|
||||
case 'Minus':
|
||||
return '-'
|
||||
case 'Equal':
|
||||
return '='
|
||||
default:
|
||||
return key
|
||||
}
|
||||
})
|
||||
}
|
||||
.join('+')
|
||||
}
|
||||
|
||||
const unregisterShortcuts = () => {
|
||||
globalShortcut.unregister('CommandOrControl+=')
|
||||
globalShortcut.unregister('CommandOrControl+numadd')
|
||||
globalShortcut.unregister('CommandOrControl+-')
|
||||
globalShortcut.unregister('CommandOrControl+numsub')
|
||||
globalShortcut.unregister('CommandOrControl+0')
|
||||
}
|
||||
export function registerShortcuts(window: BrowserWindow) {
|
||||
window.once('ready-to-show', () => {
|
||||
window.webContents.setZoomFactor(configManager.getZoomFactor())
|
||||
})
|
||||
|
||||
// Add check for window destruction
|
||||
if (mainWindow.isDestroyed()) {
|
||||
const register = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
const shortcuts = configManager.getShortcuts()
|
||||
if (!shortcuts) return
|
||||
|
||||
shortcuts.forEach((shortcut) => {
|
||||
try {
|
||||
if (shortcut.shortcut.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// When window gains focus, register shortcuts
|
||||
mainWindow.on('focus', () => {
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
registerShortcuts()
|
||||
const handler = getShortcutHandler(shortcut)
|
||||
|
||||
if (!handler) {
|
||||
return
|
||||
}
|
||||
|
||||
const accelerator = formatShortcutKey(shortcut.shortcut)
|
||||
|
||||
if (shortcut.key === 'show_app' && shortcut.enabled) {
|
||||
showAppAccelerator = accelerator
|
||||
}
|
||||
|
||||
if (shortcut.key === 'mini_window' && shortcut.enabled) {
|
||||
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) {
|
||||
const accelerator = convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(
|
||||
shortcut.shortcut
|
||||
)
|
||||
globalShortcut.register(accelerator, () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
|
||||
}
|
||||
})
|
||||
|
||||
// When window loses focus, unregister shortcuts
|
||||
mainWindow.on('blur', () => {
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
unregisterShortcuts()
|
||||
}
|
||||
})
|
||||
|
||||
// Initial registration (if window is already focused)
|
||||
if (!mainWindow.isDestroyed() && mainWindow.isFocused()) {
|
||||
registerShortcuts()
|
||||
const unregister = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
try {
|
||||
globalShortcut.unregisterAll()
|
||||
|
||||
if (showAppAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||
const accelerator =
|
||||
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showAppAccelerator)
|
||||
handler && globalShortcut.register(accelerator, () => handler(window))
|
||||
}
|
||||
|
||||
if (showMiniWindowAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
||||
const accelerator =
|
||||
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showMiniWindowAccelerator)
|
||||
handler && globalShortcut.register(accelerator, () => 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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isMac } from '@main/constant'
|
||||
import { locales } from '@main/utils/locales'
|
||||
import { app, Menu, nativeImage, nativeTheme, Tray } from 'electron'
|
||||
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'
|
||||
@@ -9,14 +9,22 @@ 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)
|
||||
|
||||
@@ -38,17 +46,25 @@ export class TrayService {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { tray: trayLocale } = locale.translation
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
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)
|
||||
@@ -61,18 +77,30 @@ export class TrayService {
|
||||
})
|
||||
|
||||
this.tray.on('click', () => {
|
||||
if (enableQuickAssistant && configManager.getClickTrayToShowQuickAssistant()) {
|
||||
windowService.showMiniWindow()
|
||||
} else {
|
||||
windowService.showMainWindow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private updateTray() {
|
||||
if (configManager.isTray()) {
|
||||
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()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import Stream from 'stream'
|
||||
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
|
||||
|
||||
import { proxyManager } from './ProxyManager'
|
||||
|
||||
export default class WebDav {
|
||||
public instance: WebDAVClient | undefined
|
||||
private webdavPath: string
|
||||
@@ -10,11 +14,16 @@ export default class WebDav {
|
||||
constructor(params: WebDavConfig) {
|
||||
this.webdavPath = params.webdavPath
|
||||
|
||||
const httpProxy = proxyManager.getProxyUrl() || ''
|
||||
const httpsProxy = proxyManager.getProxyUrl() || ''
|
||||
|
||||
this.instance = createClient(params.webdavHost, {
|
||||
username: params.webdavUser,
|
||||
password: params.webdavPass,
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity
|
||||
maxContentLength: Infinity,
|
||||
httpAgent: httpProxy ? new HttpProxyAgent(httpProxy) : undefined,
|
||||
httpsAgent: httpsProxy ? new HttpsProxyAgent(httpsProxy) : undefined
|
||||
})
|
||||
|
||||
this.putFileContents = this.putFileContents.bind(this)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { isLinux, isWin } from '@main/constant'
|
||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
import path, { join } from 'path'
|
||||
|
||||
import icon from '../../../build/icon.png?asset'
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||
@@ -12,6 +13,11 @@ 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 = ''
|
||||
private contextMenu: Menu | null = null
|
||||
|
||||
public static getInstance(): WindowService {
|
||||
if (!WindowService.instance) {
|
||||
@@ -22,6 +28,7 @@ export class WindowService {
|
||||
|
||||
public createMainWindow(): BrowserWindow {
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.show()
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
@@ -32,6 +39,7 @@ export class WindowService {
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
const isMac = process.platform === 'darwin'
|
||||
const isLinux = process.platform === 'linux'
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
@@ -40,12 +48,12 @@ export class WindowService {
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
show: false, // 初始不显示
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'under-window',
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarStyle: isLinux ? 'default' : 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
@@ -54,11 +62,13 @@ export class WindowService {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
webviewTag: true,
|
||||
allowRunningInsecureContent: true
|
||||
}
|
||||
})
|
||||
|
||||
this.setupMainWindow(this.mainWindow, mainWindowState)
|
||||
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
@@ -103,32 +113,85 @@ export class WindowService {
|
||||
}
|
||||
|
||||
private setupContextMenu(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
if (!this.contextMenu) {
|
||||
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()
|
||||
this.contextMenu = new Menu()
|
||||
this.contextMenu.append(new MenuItem({ label: common.copy, role: 'copy' }))
|
||||
this.contextMenu.append(new MenuItem({ label: common.paste, role: 'paste' }))
|
||||
this.contextMenu.append(new MenuItem({ label: common.cut, role: 'cut' }))
|
||||
}
|
||||
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
this.contextMenu?.popup()
|
||||
})
|
||||
|
||||
// Handle webview context menu
|
||||
mainWindow.webContents.on('did-attach-webview', (_, webContents) => {
|
||||
webContents.on('context-menu', () => {
|
||||
this.contextMenu?.popup()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private setupWindowEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
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
|
||||
|
||||
const oauthProviderUrls = [
|
||||
'https://account.siliconflow.cn/oauth',
|
||||
'https://cloud.siliconflow.cn/expensebill',
|
||||
'https://aihubmix.com/token',
|
||||
'https://aihubmix.com/topup'
|
||||
]
|
||||
|
||||
if (oauthProviderUrls.some((link) => url.startsWith(link))) {
|
||||
return {
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: {
|
||||
webPreferences: {
|
||||
partition: 'persist:webview'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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' }
|
||||
})
|
||||
|
||||
@@ -167,32 +230,214 @@ export class WindowService {
|
||||
|
||||
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.on('close', (event) => {
|
||||
const notInTray = !configManager.isTray()
|
||||
// 如果已经触发退出,直接退出
|
||||
if (app.isQuitting) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// Windows and Linux
|
||||
// 没有开启托盘,且是Windows或Linux系统,直接退出
|
||||
const notInTray = !configManager.getTray()
|
||||
if ((isWin || isLinux) && notInTray) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// Mac
|
||||
if (!app.isQuitting) {
|
||||
// 如果是全屏状态,直接退出
|
||||
if (this.wasFullScreen) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
})
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
this.mainWindow = null
|
||||
})
|
||||
|
||||
mainWindow.on('show', () => {
|
||||
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||
this.miniWindow.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public showMainWindow() {
|
||||
if (this.mainWindow) {
|
||||
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||
this.miniWindow.hide()
|
||||
}
|
||||
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
if (this.mainWindow.isMinimized()) {
|
||||
return this.mainWindow.restore()
|
||||
this.mainWindow.restore()
|
||||
}
|
||||
this.mainWindow.show()
|
||||
this.mainWindow.focus()
|
||||
} else {
|
||||
this.createMainWindow()
|
||||
this.mainWindow = this.createMainWindow()
|
||||
this.mainWindow.focus()
|
||||
}
|
||||
}
|
||||
|
||||
public showMiniWindow() {
|
||||
const enableQuickAssistant = configManager.getEnableQuickAssistant()
|
||||
|
||||
if (!enableQuickAssistant) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.hide()
|
||||
}
|
||||
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||
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,22 +1,19 @@
|
||||
import * as crypto from 'crypto'
|
||||
|
||||
// 定义密钥和初始化向量(IV)
|
||||
const secretKey = 'kDQvWz5slot3syfucoo53X6KKsEUJoeFikpiUWRJTLIo3zcUPpFvEa009kK13KCr'
|
||||
const iv = Buffer.from('Cherry Studio', 'hex')
|
||||
|
||||
// 加密函数
|
||||
export function encrypt(text: string): { iv: string; encryptedData: string } {
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(secretKey), iv)
|
||||
export function encrypt(text: string, secretKey: string, iv: string): { iv: string; encryptedData: string } {
|
||||
const _iv = Buffer.from(iv, 'hex')
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(secretKey), _iv)
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex')
|
||||
encrypted += cipher.final('hex')
|
||||
return {
|
||||
iv: iv.toString('hex'),
|
||||
iv: _iv.toString('hex'),
|
||||
encryptedData: encrypted
|
||||
}
|
||||
}
|
||||
|
||||
// 解密函数
|
||||
export function decrypt(encryptedData: string, iv: string): string {
|
||||
export function decrypt(encryptedData: string, iv: string, secretKey: string): string {
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(secretKey), Buffer.from(iv, 'hex'))
|
||||
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
|
||||
decrypted += decipher.final('utf8')
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@main/constant'
|
||||
import * as fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { FileTypes } from '../../renderer/src/types'
|
||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
|
||||
import { FileType, FileTypes } from '@types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export function getFileType(ext: string): FileTypes {
|
||||
ext = ext.toLowerCase()
|
||||
@@ -11,3 +14,43 @@ export function getFileType(ext: string): FileTypes {
|
||||
if (documentExts.includes(ext)) return FileTypes.DOCUMENT
|
||||
return FileTypes.OTHER
|
||||
}
|
||||
export function getAllFiles(dirPath: string, arrayOfFiles: FileType[] = []): FileType[] {
|
||||
const files = fs.readdirSync(dirPath)
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.startsWith('.')) {
|
||||
return
|
||||
}
|
||||
|
||||
const fullPath = path.join(dirPath, file)
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles)
|
||||
} else {
|
||||
const ext = path.extname(file)
|
||||
const fileType = getFileType(ext)
|
||||
|
||||
if ([FileTypes.OTHER, FileTypes.IMAGE, FileTypes.VIDEO, FileTypes.AUDIO].includes(fileType)) {
|
||||
return
|
||||
}
|
||||
|
||||
const name = path.basename(file)
|
||||
const size = fs.statSync(fullPath).size
|
||||
|
||||
const fileItem: FileType = {
|
||||
id: uuidv4(),
|
||||
name,
|
||||
path: fullPath,
|
||||
size,
|
||||
ext,
|
||||
count: 1,
|
||||
origin_name: name,
|
||||
type: fileType,
|
||||
created_at: new Date()
|
||||
}
|
||||
|
||||
arrayOfFiles.push(fileItem)
|
||||
}
|
||||
})
|
||||
|
||||
return arrayOfFiles
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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'
|
||||
@@ -7,6 +8,7 @@ const locales = {
|
||||
'en-US': EnUs,
|
||||
'zh-CN': ZhCn,
|
||||
'zh-TW': ZhTw,
|
||||
'ja-JP': JaJP,
|
||||
'ru-RU': RuRu
|
||||
}
|
||||
|
||||
|
||||
72
src/preload/index.d.ts
vendored
@@ -1,8 +1,12 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type { FileMetadataResponse, ListFilesResponse, UploadFileResponse } from '@google/generative-ai/server'
|
||||
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { FileType } from '@renderer/types'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
import { AppInfo, LanguageVarious } from '@renderer/types'
|
||||
import { AppInfo, KnowledgeBaseParams, KnowledgeItem, LanguageVarious } from '@renderer/types'
|
||||
import type { LoaderReturn } from '@shared/config/types'
|
||||
import type { OpenDialogOptions } from 'electron'
|
||||
import type { UpdateInfo } from 'electron-updater'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
declare global {
|
||||
@@ -10,11 +14,12 @@ declare global {
|
||||
electron: ElectronAPI
|
||||
api: {
|
||||
getAppInfo: () => Promise<AppInfo>
|
||||
checkForUpdate: () => void
|
||||
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
|
||||
@@ -40,6 +45,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,
|
||||
@@ -49,11 +55,73 @@ 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<LoaderReturn>
|
||||
remove: ({
|
||||
uniqueId,
|
||||
uniqueIds,
|
||||
base
|
||||
}: {
|
||||
uniqueId: string
|
||||
uniqueIds: 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>
|
||||
}
|
||||
aes: {
|
||||
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
||||
decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise<string>
|
||||
}
|
||||
shell: {
|
||||
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
@@ -10,6 +10,7 @@ const api = {
|
||||
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),
|
||||
@@ -36,18 +37,77 @@ 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)
|
||||
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, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }) =>
|
||||
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, uniqueIds, 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')
|
||||
},
|
||||
aes: {
|
||||
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
|
||||
decrypt: (encryptedData: string, iv: string, secretKey: string) =>
|
||||
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
|
||||
},
|
||||
shell: {
|
||||
openExternal: shell.openExternal
|
||||
}
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
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:" />
|
||||
<title>Cherry Studio</title>
|
||||
|
||||
<style>
|
||||
html,
|
||||
@@ -17,10 +18,10 @@
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#spinner img {
|
||||
@@ -35,6 +36,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>
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import NavigationHandler from './handler/NavigationHandler'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
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'
|
||||
@@ -27,13 +29,16 @@ function App(): JSX.Element {
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<NavigationHandler />
|
||||
{/* 添加导航处理组件 */}
|
||||
<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="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@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=1738750230250') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -11,6 +11,22 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-thinking:before {
|
||||
content: '\e65b';
|
||||
}
|
||||
|
||||
.icon-at:before {
|
||||
content: '\e623';
|
||||
}
|
||||
|
||||
.icon-icon-adaptive-width:before {
|
||||
content: '\e87a';
|
||||
}
|
||||
|
||||
.icon-at1:before {
|
||||
content: '\e630';
|
||||
}
|
||||
|
||||
.icon-a-darkmode:before {
|
||||
content: '\e6cd';
|
||||
}
|
||||
@@ -27,10 +43,6 @@
|
||||
content: '\e942';
|
||||
}
|
||||
|
||||
.icon-grid-row-2copy:before {
|
||||
content: '\e681';
|
||||
}
|
||||
|
||||
.icon-inbox:before {
|
||||
content: '\e869';
|
||||
}
|
||||
@@ -71,10 +83,6 @@
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
.icon-a-addchat:before {
|
||||
content: '\e658';
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: '\e792';
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB |
BIN
src/renderer/src/assets/images/apps/3mintop.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/renderer/src/assets/images/apps/abacus.webp
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
27
src/renderer/src/assets/images/apps/aistudio.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="256" height="256" rx="32" fill="#0057CE"/>
|
||||
<mask id="path-2-inside-1_4113_89308" fill="white">
|
||||
<path d="M169.6 131.626C173.075 129.641 176.32 128.241 180.1 126.943C183.74 125.695 187.444 124.664 191.186 123.735C194.915 122.806 198.682 122.017 202.449 121.228C206.216 120.439 209.958 119.675 213.598 118.314C231.429 111.619 242.221 93.6357 239.612 74.9396C237.003 56.2435 221.692 41.8237 202.691 40.1564C194.062 39.4055 185.726 41.4164 178.013 44.9418C170.326 48.4545 163.288 53.4435 157.166 59.158C144.795 70.676 135.657 85.4649 130.083 101.208C124.47 117.054 122.37 134.095 123.694 150.806C124.356 159.129 125.883 167.504 128.326 175.509C130.719 183.362 134.181 191.469 138.839 198.342C136.828 185.475 138.559 172.175 143.917 160.262C149.262 148.375 158.121 138.193 169.6 131.626Z"/>
|
||||
</mask>
|
||||
<path d="M169.6 131.626C173.075 129.641 176.32 128.241 180.1 126.943C183.74 125.695 187.444 124.664 191.186 123.735C194.915 122.806 198.682 122.017 202.449 121.228C206.216 120.439 209.958 119.675 213.598 118.314C231.429 111.619 242.221 93.6357 239.612 74.9396C237.003 56.2435 221.692 41.8237 202.691 40.1564C194.062 39.4055 185.726 41.4164 178.013 44.9418C170.326 48.4545 163.288 53.4435 157.166 59.158C144.795 70.676 135.657 85.4649 130.083 101.208C124.47 117.054 122.37 134.095 123.694 150.806C124.356 159.129 125.883 167.504 128.326 175.509C130.719 183.362 134.181 191.469 138.839 198.342C136.828 185.475 138.559 172.175 143.917 160.262C149.262 148.375 158.121 138.193 169.6 131.626Z" fill="white" stroke="white" stroke-width="32" mask="url(#path-2-inside-1_4113_89308)"/>
|
||||
<path d="M162.246 150.4C161.915 153.913 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z" fill="white"/>
|
||||
<mask id="path-4-outside-2_4113_89308" maskUnits="userSpaceOnUse" x="136" y="138.4" width="71" height="92" fill="black">
|
||||
<rect fill="white" x="136" y="138.4" width="71" height="92"/>
|
||||
<path d="M162.246 150.4C165.542 153.666 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z"/>
|
||||
</mask>
|
||||
<path d="M162.246 150.4C165.542 153.666 163.073 157.464 165.542 160.06C168.011 162.657 171.499 164.031 174.668 165.253C178.13 166.577 181.12 167.658 184.353 169.529C187.433 171.311 190.157 173.526 192.435 176.262C201.802 187.449 200.937 203.867 190.462 214.049C179.988 224.23 163.379 224.778 152.243 215.321C149.404 212.903 146.884 209.798 144.81 206.756C141.654 186.52 147.775 165.317 162.246 150.4Z" stroke="#0057CE" stroke-width="16" mask="url(#path-4-outside-2_4113_89308)"/>
|
||||
<mask id="path-5-inside-3_4113_89308" fill="white">
|
||||
<path d="M50.4113 61.9063C63.3547 61.8935 75.9164 69.008 85.0163 76.9879C94.6761 85.4641 102.16 96.2567 107.085 107.991C112.036 119.789 114.416 132.542 114.327 145.282C114.238 157.665 111.769 171.079 106.296 182.394C105.774 167.821 100.123 153.885 90.3107 143.003C88.5926 141.107 86.7981 139.389 84.6599 137.938C82.5218 136.487 80.2691 135.418 77.8382 134.565C73.1164 132.911 67.7838 132.134 62.8711 131.6C57.8057 131.04 52.7149 130.709 47.6622 129.971C42.4695 129.207 37.8114 128.087 33.1787 125.427C19.688 117.715 13.1463 102.009 17.1808 87.1441C21.2153 72.2661 34.846 61.919 50.4113 61.9063Z"/>
|
||||
</mask>
|
||||
<path d="M50.4113 61.9063C63.3547 61.8935 75.9164 69.008 85.0163 76.9879C94.6761 85.4641 102.16 96.2567 107.085 107.991C112.036 119.789 114.416 132.542 114.327 145.282C114.238 157.665 111.769 171.079 106.296 182.394C105.774 167.821 100.123 153.885 90.3107 143.003C88.5926 141.107 86.7981 139.389 84.6599 137.938C82.5218 136.487 80.2691 135.418 77.8382 134.565C73.1164 132.911 67.7838 132.134 62.8711 131.6C57.8057 131.04 52.7149 130.709 47.6622 129.971C42.4695 129.207 37.8114 128.087 33.1787 125.427C19.688 117.715 13.1463 102.009 17.1808 87.1441C21.2153 72.2661 34.846 61.919 50.4113 61.9063Z" fill="white" stroke="white" stroke-width="32" mask="url(#path-5-inside-3_4113_89308)"/>
|
||||
<mask id="path-6-inside-4_4113_89308" fill="white">
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z"/>
|
||||
</mask>
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" stroke="white" stroke-width="24" mask="url(#path-6-inside-4_4113_89308)"/>
|
||||
<mask id="path-7-outside-5_4113_89308" maskUnits="userSpaceOnUse" x="45.3994" y="138.6" width="62" height="79" fill="black">
|
||||
<rect fill="white" x="45.3994" y="138.6" width="62" height="79"/>
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z"/>
|
||||
</mask>
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" fill="white"/>
|
||||
<path d="M82.5802 149.38C81.3584 148.03 80.0857 146.745 78.673 145.6C80.4294 148.578 80.6075 151.95 79.8694 155.196C79.1312 158.429 77.5021 161.419 75.4403 163.99C73.3149 166.625 70.8204 168.725 68.1095 170.71C65.7423 172.441 62.2932 174.656 60.1551 176.73C53.8679 182.839 52.5824 192.384 57.0369 199.893C61.4914 207.415 70.5277 210.979 78.9912 208.535C83.662 207.186 87.6202 204.144 90.7638 200.67C93.9455 197.157 96.5291 192.983 98.5655 188.757C98.0437 174.185 92.3928 160.261 82.5802 149.38Z" stroke="#0057CE" stroke-width="16" mask="url(#path-7-outside-5_4113_89308)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src/renderer/src/assets/images/apps/baidu-ai-search.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/renderer/src/assets/images/apps/coze.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
src/renderer/src/assets/images/apps/dify.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Dify</title><clipPath id="lobe-icons-dify-fill"><path d="M1 0h10.286c6.627 0 12 5.373 12 12s-5.373 12-12 12H1V0z"></path></clipPath><foreignObject clip-path="url(#lobe-icons-dify-fill)" height="24" style="background:conic-gradient(from 180deg at 50% 50%, #0222C3, #8FB1F4, #FFFFFF)" width="24"></foreignObject></svg>
|
||||
|
After Width: | Height: | Size: 480 B |
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: 7.9 KiB |
BIN
src/renderer/src/assets/images/apps/hika.webp
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 20 KiB |
BIN
src/renderer/src/assets/images/apps/kimi.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/renderer/src/assets/images/apps/lambdachat.webp
Normal file
|
After Width: | Height: | Size: 724 B |
BIN
src/renderer/src/assets/images/apps/lechat.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/renderer/src/assets/images/apps/monica.webp
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/renderer/src/assets/images/apps/nm-search.webp
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/apps/nm.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
7
src/renderer/src/assets/images/apps/notebooklm.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512px" height="512px" viewBox="0 0 512 512" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(85.09804%,85.09804%,85.09804%);fill-opacity:1;" d="M 512 256 C 512 114.613281 397.386719 0 256 0 C 114.613281 0 0 114.613281 0 256 C 0 397.386719 114.613281 512 256 512 C 397.386719 512 512 397.386719 512 256 Z M 512 256 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 256.011719 114.753906 C 167.050781 114.753906 94.945312 186.261719 94.945312 274.507812 L 94.945312 350.988281 L 124.628906 350.988281 L 124.628906 343.359375 C 124.628906 307.574219 153.867188 278.558594 189.941406 278.558594 C 226.015625 278.558594 255.253906 307.585938 255.253906 343.359375 L 255.253906 350.988281 L 284.9375 350.988281 L 284.9375 343.359375 C 284.9375 291.308594 242.390625 249.140625 189.929688 249.140625 C 169.503906 249.140625 150.582031 255.53125 135.082031 266.433594 C 151.296875 234.464844 184.691406 212.535156 223.242188 212.535156 C 277.707031 212.535156 321.867188 256.339844 321.867188 310.355469 L 321.867188 350.996094 L 351.5625 350.996094 L 351.5625 310.355469 C 351.5625 240.074219 294.113281 183.082031 223.242188 183.082031 C 191.382812 183.082031 162.230469 194.601562 139.785156 213.683594 C 161.824219 172.375 205.578125 144.214844 256 144.214844 C 328.566406 144.214844 387.382812 202.550781 387.382812 274.515625 L 387.382812 350.996094 L 417.066406 350.996094 L 417.066406 274.515625 C 417.066406 186.28125 344.960938 114.761719 256 114.761719 Z M 256.011719 114.753906 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/renderer/src/assets/images/apps/qwenlm.webp
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/apps/sparkdesk.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/apps/thinkany.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |