Compare commits
336 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a25f4e90dd | ||
|
|
3d701b98aa | ||
|
|
dcaac54c75 | ||
|
|
b2b89a1339 | ||
|
|
4692f98770 | ||
|
|
86a3a108a7 | ||
|
|
5ec4403bfb | ||
|
|
ec0be1ff27 | ||
|
|
516315ac45 | ||
|
|
ff55739376 | ||
|
|
1e24b7bc45 | ||
|
|
dd92dca34b | ||
|
|
a592fdc550 | ||
|
|
846e7ca097 | ||
|
|
93c2a94658 | ||
|
|
309b66e4df | ||
|
|
4d9476e99b | ||
|
|
46f796a74c | ||
|
|
00bf28b999 | ||
|
|
640d3783a0 | ||
|
|
0e4f06e86a | ||
|
|
886a7ec1e9 | ||
|
|
37cf7427f9 | ||
|
|
e69d0c89a6 | ||
|
|
581e2fb786 | ||
|
|
13b465fe73 | ||
|
|
a12d10f4f7 | ||
|
|
e8bfb2b49b | ||
|
|
ae995182b2 | ||
|
|
59c69e065c | ||
|
|
4ca2d61ccc | ||
|
|
d62ff69351 | ||
|
|
012e79a7e2 | ||
|
|
97dc80a07f | ||
|
|
b974f8537f | ||
|
|
c32e17968e | ||
|
|
cf09d1d44d | ||
|
|
ad39d8774d | ||
|
|
687f140a5c | ||
|
|
1b09bb47bf | ||
|
|
808b457503 | ||
|
|
92e054569c | ||
|
|
2f22e68559 | ||
|
|
53a8628fab | ||
|
|
df04503674 | ||
|
|
1fe74fa753 | ||
|
|
55a9be2fa5 | ||
|
|
fdf4821d56 | ||
|
|
84e6caa846 | ||
|
|
3bc8dfdf8c | ||
|
|
ed96940e82 | ||
|
|
1c60375d71 | ||
|
|
c9699609ed | ||
|
|
f91caff7ec | ||
|
|
11bd55701c | ||
|
|
efa9c6c546 | ||
|
|
92ab67eb3d | ||
|
|
ae11490f87 | ||
|
|
956c2f683d | ||
|
|
d01f793558 | ||
|
|
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 |
@@ -16,6 +16,7 @@ module.exports = {
|
|||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
'simple-import-sort/imports': 'error',
|
'simple-import-sort/imports': 'error',
|
||||||
'simple-import-sort/exports': 'error',
|
'simple-import-sort/exports': 'error',
|
||||||
'react/no-is-mounted': 'off'
|
'react/no-is-mounted': 'off',
|
||||||
|
'prettier/prettier': ['error', { endOfLine: 'auto' }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: 🐛 错误报告
|
name: 🐛 错误报告 (中文)
|
||||||
description: 创建一个报告以帮助我们改进
|
description: 创建一个报告以帮助我们改进
|
||||||
title: '[错误]: '
|
title: '[错误]: '
|
||||||
labels: ['bug']
|
labels: ['bug']
|
||||||
@@ -7,17 +7,20 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
感谢您花时间填写此错误报告!
|
感谢您花时间填写此错误报告!
|
||||||
|
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue 检查清单
|
label: 提交前检查
|
||||||
description: |
|
description: |
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
options:
|
options:
|
||||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
required: true
|
required: true
|
||||||
- label: 正确填写了 Issue 标题。
|
- 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
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@@ -45,7 +48,7 @@ body:
|
|||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: 错误描述
|
label: 错误描述
|
||||||
description: 清晰简洁地描述错误是什么
|
description: 描述问题时请尽可能详细
|
||||||
placeholder: 告诉我们发生了什么...
|
placeholder: 告诉我们发生了什么...
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -54,7 +57,7 @@ body:
|
|||||||
id: reproduction
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
label: 重现步骤
|
label: 重现步骤
|
||||||
description: 重现行为的步骤
|
description: 提供详细的重现步骤,以便于我们可以准确地重现问题
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. 转到 '...'
|
1. 转到 '...'
|
||||||
2. 点击 '....'
|
2. 点击 '....'
|
||||||
@@ -82,4 +85,4 @@ body:
|
|||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: 附加信息
|
label: 附加信息
|
||||||
description: 在此添加有关问题的任何其他上下文
|
description: 任何能让我们对你所遇到的问题有更多了解的东西
|
||||||
|
|||||||
36
.github/ISSUE_TEMPLATE/#1_feature_request.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: 💡 功能建议
|
name: 💡 功能建议 (中文)
|
||||||
description: 为项目提出新的想法
|
description: 为项目提出新的想法
|
||||||
title: '[功能]: '
|
title: '[功能]: '
|
||||||
labels: ['enhancement']
|
labels: ['enhancement']
|
||||||
@@ -7,23 +7,49 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
感谢您花时间提出新的功能建议!
|
感谢您花时间提出新的功能建议!
|
||||||
|
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue 检查清单
|
label: 提交前检查
|
||||||
description: |
|
description: |
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
options:
|
options:
|
||||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
required: true
|
required: true
|
||||||
- label: 正确填写了 Issue 标题。
|
- 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
|
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
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
label: 您的功能建议是否与某个问题相关?
|
label: 您的功能建议是否与某个问题/issue相关?
|
||||||
description: 请简明扼要地描述您遇到的问题
|
description: 请简明扼要地描述您遇到的问题
|
||||||
placeholder: 我总是感到沮丧,因为...
|
placeholder: 我总是感到沮丧,因为...
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
31
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: ❓ 提问
|
name: ❓ 讨论 & 提问 (中文)
|
||||||
description: 提出一个问题或寻求帮助
|
description: 寻求帮助、讨论问题、提出疑问等...
|
||||||
title: '[问题]: '
|
title: '[讨论]: '
|
||||||
labels: ['question']
|
labels: ['question']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@@ -15,11 +15,32 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
options:
|
options:
|
||||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
required: true
|
required: true
|
||||||
- label: 正确填写了 Issue 标题。
|
- label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议。
|
||||||
required: true
|
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
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
21
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: 🐛 Bug Report
|
name: 🐛 Bug Report (English)
|
||||||
description: Create a report to help us improve
|
description: Create a report to help us improve
|
||||||
title: '[Bug]: '
|
title: '[Bug]: '
|
||||||
labels: ['bug']
|
labels: ['bug']
|
||||||
@@ -6,7 +6,8 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
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
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@@ -15,9 +16,11 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Before submitting an issue, please make sure you have completed the following steps
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
options:
|
options:
|
||||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
- 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
|
required: true
|
||||||
- label: I have filled out the issue title correctly.
|
- 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
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@@ -45,8 +48,8 @@ body:
|
|||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Bug Description
|
label: Bug Description
|
||||||
description: A clear and concise description of what the bug is
|
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...
|
placeholder: Tell us what happened... (Remember to attach screenshots/recordings if applicable)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -54,12 +57,14 @@ body:
|
|||||||
id: reproduction
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps To Reproduce
|
label: Steps To Reproduce
|
||||||
description: Steps to reproduce the behavior
|
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: |
|
placeholder: |
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
|
Remember to attach screenshots/recordings for each step when possible!
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -82,4 +87,4 @@ body:
|
|||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Context
|
label: Additional Context
|
||||||
description: Add any other context about the problem here
|
description: Anything that gives us a better understanding of the problem you're experiencing
|
||||||
|
|||||||
52
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: 💡 Feature Request
|
name: 💡 Feature Request (English)
|
||||||
description: Suggest an idea for this project
|
description: Suggest an idea for this project
|
||||||
title: '[Feature]: '
|
title: '[Feature]: '
|
||||||
labels: ['enhancement']
|
labels: ['enhancement']
|
||||||
@@ -6,7 +6,8 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to suggest a new feature!
|
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
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@@ -15,36 +16,61 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Before submitting an issue, please make sure you have completed the following steps
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
options:
|
options:
|
||||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
- 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
|
required: true
|
||||||
- label: I have filled out the issue title correctly.
|
- 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
|
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
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
label: Is your feature request related to a problem?
|
label: Is your feature request related to an existing issue?
|
||||||
description: A clear and concise description of what the problem is
|
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'm always frustrated when...
|
placeholder: I often feel frustrated because... (Remember to attach screenshots/recordings if applicable)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: solution
|
id: solution
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe the solution you'd like
|
label: Desired Solution
|
||||||
description: A clear and concise description of what you want to happen
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: alternatives
|
id: alternatives
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe alternatives you've considered
|
label: Alternative Solutions
|
||||||
description: A clear and concise description of any alternative solutions or features you've considered
|
description: Please briefly describe any alternative solutions or features you have considered. Feel free to include screenshots or mockups of alternative approaches.
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Context
|
label: Additional Information
|
||||||
description: Add any other context or screenshots about the feature request here
|
description: Add any other context, screenshots, mockups or recordings that can help us better understand your feature request.
|
||||||
|
|||||||
49
.github/ISSUE_TEMPLATE/2_question.yml
vendored
@@ -1,12 +1,12 @@
|
|||||||
name: ❓ Question
|
name: ❓ Discussion & Questions
|
||||||
description: Ask a question or seek help
|
description: Seeking help, discussing issues, asking questions, etc...
|
||||||
title: '[Question]: '
|
title: '[Discussion]: '
|
||||||
labels: ['question']
|
labels: ['question']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for asking a question! Please provide as much detail as possible so we can better assist you.
|
Thank you for your question! Please describe your issue in as much detail as possible so that we can better assist you.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@@ -15,17 +15,40 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Before submitting an issue, please make sure you have completed the following steps
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
options:
|
options:
|
||||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
- 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
|
required: true
|
||||||
- label: I have filled out the issue title correctly.
|
- 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
|
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
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
attributes:
|
attributes:
|
||||||
label: Your Question
|
label: Your Question
|
||||||
description: Please describe your question in detail
|
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 question as clearly as possible...
|
placeholder: Please explain your issue as clearly as possible...(Remember to attach screenshots/recordings if applicable)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -33,23 +56,23 @@ body:
|
|||||||
id: context
|
id: context
|
||||||
attributes:
|
attributes:
|
||||||
label: Context
|
label: Context
|
||||||
description: Please provide some background information to help us better understand your question
|
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."
|
placeholder: "For example: use case, solutions you've tried, etc. Don't forget to include relevant screenshots/recordings!"
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Information
|
label: Additional Information
|
||||||
description: Any other relevant information, screenshots, or code examples
|
description: Any other relevant information, screenshots, recordings, or code examples that can help us better assist you
|
||||||
render: shell
|
render: shell
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: priority
|
id: priority
|
||||||
attributes:
|
attributes:
|
||||||
label: Priority
|
label: Priority
|
||||||
description: How urgent is this question for you?
|
description: How urgent is this issue for you?
|
||||||
options:
|
options:
|
||||||
- Low (Can wait)
|
- Low (Review when available)
|
||||||
- Medium (Would like a response soon)
|
- Medium (Would like a response soon)
|
||||||
- High (Blocking progress)
|
- High (Blocking progress)
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -44,3 +44,6 @@ stats.html
|
|||||||
|
|
||||||
# Local
|
# Local
|
||||||
local
|
local
|
||||||
|
.aider*
|
||||||
|
.cursorrules
|
||||||
|
.cursor/rules
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
README.md
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
||||||
|
|
||||||
👏 Join [Telegram Group](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ Group(1022779719)](https://qm.qq.com/q/Qtw8As0cwe)
|
👏 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!
|
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
|||||||
|
|
||||||
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
||||||
- 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others
|
- 🔗 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**:
|
2. **AI Assistants & Conversations**:
|
||||||
|
|
||||||
@@ -60,6 +60,21 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
|||||||
- 📝 Complete Markdown Rendering
|
- 📝 Complete Markdown Rendering
|
||||||
- 🤲 Easy Content Sharing
|
- 🤲 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
|
# 🖥️ Develop
|
||||||
|
|
||||||
## IDE Setup
|
## IDE Setup
|
||||||
@@ -117,7 +132,8 @@ For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIB
|
|||||||
Thank you for your support and contributions!
|
Thank you for your support and contributions!
|
||||||
|
|
||||||
## Related Projects
|
## 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.
|
|
||||||
|
- [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
|
# 🚀 Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。
|
Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。
|
||||||
|
|
||||||
👏 [Telegram](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(1022779719)](https://qm.qq.com/q/Qtw8As0cwe)
|
👏 [Telegram](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(1025067911)](https://qm.qq.com/q/RIBAO2pPKS)
|
||||||
|
|
||||||
❤️ Cherry Studioをお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
|
❤️ Cherry Studioをお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
|
|||||||
|
|
||||||
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
|
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
|
||||||
- 🔗 AI Web サービス統合:Claude、Peplexity、Poe など
|
- 🔗 AI Web サービス統合:Claude、Peplexity、Poe など
|
||||||
- 💻 Ollama によるローカルモデル実行対応
|
- 💻 Ollama、LM Studio によるローカルモデル実行対応
|
||||||
|
|
||||||
2. **AI アシスタントと対話**:
|
2. **AI アシスタントと対話**:
|
||||||
|
|
||||||
@@ -61,6 +61,21 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
|
|||||||
- 📝 完全な Markdown レンダリング
|
- 📝 完全な Markdown レンダリング
|
||||||
- 🤲 簡単な共有機能
|
- 🤲 簡単な共有機能
|
||||||
|
|
||||||
|
# 📝 TODO
|
||||||
|
|
||||||
|
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
|
||||||
|
- [x] 複数モデルの回答の比較
|
||||||
|
- [x] サービスプロバイダーが提供するSSOを使用したログインをサポート
|
||||||
|
- [x] すべてのモデルがネットワークをサポート
|
||||||
|
- [x] 最初の公式バージョンのリリース
|
||||||
|
- [ ] 錯誤修復と改善 (開発中...)
|
||||||
|
- [ ] プラグイン機能(JavaScript)
|
||||||
|
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
|
||||||
|
- [ ] iOS & Android クライアント
|
||||||
|
- [ ] AIノート
|
||||||
|
- [ ] 音声入出力(AIコール)
|
||||||
|
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
|
||||||
|
|
||||||
# 🖥️ 開発
|
# 🖥️ 開発
|
||||||
|
|
||||||
## IDEの設定
|
## IDEの設定
|
||||||
@@ -118,7 +133,8 @@ Cherry Studioへの貢献を歓迎します!以下の方法で貢献できま
|
|||||||
ご支援と貢献に感謝します!
|
ご支援と貢献に感謝します!
|
||||||
|
|
||||||
## 関連頁版
|
## 関連頁版
|
||||||
* [one-api](https://github.com/songquanpeng/one-api):LLM APIの管理・配信システム。OpenAI、Azure、Anthropicなどの主要モデルに対応し、統一APIインターフェースを提供。APIキー管理と再配布に利用可能。
|
|
||||||
|
- [one-api](https://github.com/songquanpeng/one-api):LLM APIの管理・配信システム。OpenAI、Azure、Anthropicなどの主要モデルに対応し、統一APIインターフェースを提供。APIキー管理と再配布に利用可能。
|
||||||
|
|
||||||
# 🚀 コントリビューター
|
# 🚀 コントリビューター
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。
|
Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。
|
||||||
|
|
||||||
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(1022779719)](https://qm.qq.com/q/Qtw8As0cwe)
|
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(1025067911)](https://qm.qq.com/q/RIBAO2pPKS)
|
||||||
|
|
||||||
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
|
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
|||||||
|
|
||||||
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||||
- 🔗 集成流行 AI Web 服务:Claude、Peplexity、Poe、腾讯元宝、知乎直答等
|
- 🔗 集成流行 AI Web 服务:Claude、Peplexity、Poe、腾讯元宝、知乎直答等
|
||||||
- 💻 支持 Ollama 本地模型部署
|
- 💻 支持 Ollama、LM Studio 本地模型部署
|
||||||
|
|
||||||
2. **智能助手与对话**:
|
2. **智能助手与对话**:
|
||||||
|
|
||||||
@@ -61,6 +61,21 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
|||||||
- 📝 完整的 Markdown 渲染
|
- 📝 完整的 Markdown 渲染
|
||||||
- 🤲 便捷的内容分享功能
|
- 🤲 便捷的内容分享功能
|
||||||
|
|
||||||
|
# 📝 待辦事項
|
||||||
|
|
||||||
|
- [x] 快捷弹窗 (读取剪贴板、快速提问、解释、翻译、总结)
|
||||||
|
- [x] 多模型回答对比
|
||||||
|
- [x] 支持使用服务供应商提供的 SSO 进行登入
|
||||||
|
- [x] 全部模型支持连网(开发中...)
|
||||||
|
- [x] 推出第一个正式版
|
||||||
|
- [x] 错误修复和改进(开发中...)
|
||||||
|
- [ ] 插件功能(JavaScript)
|
||||||
|
- [ ] 浏览器插件(划词翻译、总结、新增至知识库)
|
||||||
|
- [ ] iOS & Android 客户端
|
||||||
|
- [ ] AI 笔记
|
||||||
|
- [ ] 语音输入输出(AI 通话)
|
||||||
|
- [ ] 数据备份支持自定义备份内容
|
||||||
|
|
||||||
# 🖥️ 开发
|
# 🖥️ 开发
|
||||||
|
|
||||||
## IDE 设置
|
## IDE 设置
|
||||||
@@ -118,7 +133,8 @@ $ yarn build:linux
|
|||||||
感谢您的支持和贡献!
|
感谢您的支持和贡献!
|
||||||
|
|
||||||
## 相关项目
|
## 相关项目
|
||||||
* [one-api](https://github.com/songquanpeng/one-api):LLM API管理及分发系统,支持OpenAI、Azure、Anthropic等主流模型,统一API接口,可用于密钥管理与二次分发。
|
|
||||||
|
- [one-api](https://github.com/songquanpeng/one-api):LLM API管理及分发系统,支持OpenAI、Azure、Anthropic等主流模型,统一API接口,可用于密钥管理与二次分发。
|
||||||
|
|
||||||
# 🚀 贡献者
|
# 🚀 贡献者
|
||||||
|
|
||||||
|
|||||||
174
docs/technical/KnowledgeService.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
我来详细介绍 KnowledgeService 中控制并发处理的项目数量和工作负载的部分。
|
||||||
|
|
||||||
|
## 并发控制与工作负载管理机制
|
||||||
|
|
||||||
|
KnowledgeService 实现了一个精细的任务队列系统,用于控制并发处理的项目数量和工作负载。这个系统主要通过以下几个关键部分实现:
|
||||||
|
|
||||||
|
### 1. 关键变量和限制
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private workload = 0
|
||||||
|
private processingItemCount = 0
|
||||||
|
private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map()
|
||||||
|
private static MAXIMUM_WORKLOAD = 1024 * 1024 * 80 // 约80MB
|
||||||
|
private static MAXIMUM_PROCESSING_ITEM_COUNT = 30
|
||||||
|
```
|
||||||
|
|
||||||
|
- `workload`: 跟踪当前正在处理的总工作量(以字节为单位)
|
||||||
|
- `processingItemCount`: 跟踪当前正在处理的项目数量
|
||||||
|
- `MAXIMUM_WORKLOAD`: 设置最大工作负载为80MB
|
||||||
|
- `MAXIMUM_PROCESSING_ITEM_COUNT`: 设置最大并发处理项目数为30个
|
||||||
|
|
||||||
|
### 2. 工作负载评估
|
||||||
|
|
||||||
|
每个任务都有一个评估工作负载的机制,通过 `evaluateTaskWorkload` 属性来表示:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface EvaluateTaskWorkload {
|
||||||
|
workload: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
不同类型的任务有不同的工作负载评估方式:
|
||||||
|
|
||||||
|
- 文件任务:使用文件大小作为工作负载 `{ workload: file.size }`
|
||||||
|
- URL任务:使用固定值 `{ workload: 1024 * 1024 * 2 }` (约2MB)
|
||||||
|
- 网站地图任务:使用固定值 `{ workload: 1024 * 1024 * 20 }` (约20MB)
|
||||||
|
- 笔记任务:使用文本内容的字节长度 `{ workload: contentBytes.length }`
|
||||||
|
|
||||||
|
### 3. 任务状态管理
|
||||||
|
|
||||||
|
任务通过状态枚举来跟踪其生命周期:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum LoaderTaskItemState {
|
||||||
|
PENDING, // 等待处理
|
||||||
|
PROCESSING, // 正在处理
|
||||||
|
DONE // 已完成
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 任务队列处理核心逻辑
|
||||||
|
|
||||||
|
核心的队列处理逻辑在 `processingQueueHandle` 方法中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private processingQueueHandle() {
|
||||||
|
const getSubtasksUntilMaximumLoad = (): QueueTaskItem[] => {
|
||||||
|
const queueTaskList: QueueTaskItem[] = []
|
||||||
|
that: for (const [task, resolve] of this.knowledgeItemProcessingQueueMappingPromise) {
|
||||||
|
for (const item of task.loaderTasks) {
|
||||||
|
if (this.maximumLoad()) {
|
||||||
|
break that
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state, task: taskPromise, evaluateTaskWorkload } = item
|
||||||
|
|
||||||
|
if (state !== LoaderTaskItemState.PENDING) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workload } = evaluateTaskWorkload
|
||||||
|
this.workload += workload
|
||||||
|
this.processingItemCount += 1
|
||||||
|
item.state = LoaderTaskItemState.PROCESSING
|
||||||
|
queueTaskList.push({
|
||||||
|
taskPromise: () =>
|
||||||
|
taskPromise().then(() => {
|
||||||
|
this.workload -= workload
|
||||||
|
this.processingItemCount -= 1
|
||||||
|
task.loaderTasks.delete(item)
|
||||||
|
if (task.loaderTasks.size === 0) {
|
||||||
|
this.knowledgeItemProcessingQueueMappingPromise.delete(task)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
this.processingQueueHandle()
|
||||||
|
}),
|
||||||
|
resolve: () => {},
|
||||||
|
evaluateTaskWorkload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queueTaskList
|
||||||
|
}
|
||||||
|
|
||||||
|
const subTasks = getSubtasksUntilMaximumLoad()
|
||||||
|
if (subTasks.length > 0) {
|
||||||
|
const subTaskPromises = subTasks.map(({ taskPromise }) => taskPromise())
|
||||||
|
Promise.all(subTaskPromises).then(() => {
|
||||||
|
subTasks.forEach(({ resolve }) => resolve())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个方法的工作流程是:
|
||||||
|
|
||||||
|
1. 遍历所有待处理的任务集合
|
||||||
|
2. 对于每个任务集合中的每个子任务:
|
||||||
|
- 检查是否已达到最大负载(通过 `maximumLoad()` 方法)
|
||||||
|
- 如果任务状态为 PENDING,则:
|
||||||
|
- 增加当前工作负载和处理项目计数
|
||||||
|
- 将任务状态更新为 PROCESSING
|
||||||
|
- 将任务添加到待执行队列
|
||||||
|
3. 执行所有收集到的子任务
|
||||||
|
4. 当子任务完成时:
|
||||||
|
- 减少工作负载和处理项目计数
|
||||||
|
- 从任务集合中移除已完成的任务
|
||||||
|
- 如果任务集合为空,则解析相应的 Promise
|
||||||
|
- 递归调用 `processingQueueHandle()` 以处理更多任务
|
||||||
|
|
||||||
|
### 5. 负载检查
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private maximumLoad() {
|
||||||
|
return (
|
||||||
|
this.processingItemCount >= KnowledgeService.MAXIMUM_PROCESSING_ITEM_COUNT ||
|
||||||
|
this.workload >= KnowledgeService.MAXIMUM_WORKLOAD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个方法检查当前是否已达到最大负载,通过两个条件:
|
||||||
|
|
||||||
|
- 处理项目数量达到上限(30个)
|
||||||
|
- 总工作负载达到上限(80MB)
|
||||||
|
|
||||||
|
### 6. 任务添加与执行流程
|
||||||
|
|
||||||
|
当添加新任务时,流程如下:
|
||||||
|
|
||||||
|
1. 创建任务(根据类型不同创建不同的任务)
|
||||||
|
2. 通过 `appendProcessingQueue` 将任务添加到队列
|
||||||
|
3. 调用 `processingQueueHandle` 开始处理队列中的任务
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private appendProcessingQueue(task: LoaderTask): Promise<LoaderReturn> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.knowledgeItemProcessingQueueMappingPromise.set(loaderTaskIntoOfSet(task), () => {
|
||||||
|
resolve(task.loaderDoneReturn!)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 并发控制的优势
|
||||||
|
|
||||||
|
这种并发控制机制有几个重要优势:
|
||||||
|
|
||||||
|
1. **资源使用优化**:通过限制同时处理的项目数量和总工作负载,避免系统资源过度使用
|
||||||
|
2. **自动调节**:当任务完成时,会自动从队列中获取新任务,保持资源的高效利用
|
||||||
|
3. **灵活性**:不同类型的任务有不同的工作负载评估,更准确地反映实际资源需求
|
||||||
|
4. **可靠性**:通过状态管理和Promise解析机制,确保任务正确完成并通知调用者
|
||||||
|
|
||||||
|
## 实际应用场景
|
||||||
|
|
||||||
|
这种并发控制在处理大量数据时特别有用,例如:
|
||||||
|
|
||||||
|
- 导入大型目录时,可能包含数百个文件
|
||||||
|
- 处理大型网站地图,可能包含大量URL
|
||||||
|
- 处理多个用户同时添加知识库项目的请求
|
||||||
|
|
||||||
|
通过这种机制,系统可以平滑地处理大量请求,避免资源耗尽,同时保持良好的响应性。
|
||||||
|
|
||||||
|
总结来说,KnowledgeService 实现了一个复杂而高效的任务队列系统,通过精确控制并发处理的项目数量和工作负载,确保系统在处理大量数据时保持稳定和高效。
|
||||||
@@ -24,9 +24,9 @@ files:
|
|||||||
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
|
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
|
||||||
- '!node_modules/rollup-plugin-visualizer'
|
- '!node_modules/rollup-plugin-visualizer'
|
||||||
- '!node_modules/js-tiktoken'
|
- '!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/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/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
|
||||||
- '!node_modules/html2canvas/dist/{html2canvas.min.js,html2canvas.esm.js}'
|
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
- '**/*.{node,dll,metal,exp,lib}'
|
- '**/*.{node,dll,metal,exp,lib}'
|
||||||
@@ -80,11 +80,9 @@ afterPack: scripts/after-pack.js
|
|||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
知识库增加更多文件类型支持
|
数据备份和恢复支持进度显示
|
||||||
使用@呼出模型选择列表
|
支持快捷引用模型回复内容
|
||||||
添加话题固定功能
|
输入框可以手动调整大小
|
||||||
增加导出话题至Notion的功能
|
知识库文件支持一键删除
|
||||||
增加 Google AI Studio 小程序
|
服务商列表支持查询(拖拽可排序)
|
||||||
增加 Gitee 服务商
|
增加代码块换行功能
|
||||||
增加 PPIO 服务商
|
|
||||||
为 OpenAI 请求添加引用来源数据显示
|
|
||||||
|
|||||||
@@ -43,7 +43,24 @@ export default defineConfig({
|
|||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
plugins: [react(), ...visualizerPlugin('renderer')],
|
plugins: [
|
||||||
|
react({
|
||||||
|
babel: {
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
'styled-components',
|
||||||
|
{
|
||||||
|
displayName: true, // 开发环境下启用组件名称
|
||||||
|
fileName: false, // 不在类名中包含文件名
|
||||||
|
pure: true, // 优化性能
|
||||||
|
ssr: false // 不需要服务端渲染
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...visualizerPlugin('renderer')
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@renderer': resolve('src/renderer/src'),
|
'@renderer': resolve('src/renderer/src'),
|
||||||
@@ -51,7 +68,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: ['chunk-PZ64DZKH.js', 'chunk-JMKENWIY.js']
|
exclude: ['chunk-PZ64DZKH.js', 'chunk-JMKENWIY.js', 'chunk-UXYB6GHG.js']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "0.9.23",
|
"version": "1.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@electron/notarize": "^2.5.0",
|
"@electron/notarize": "^2.5.0",
|
||||||
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@google/generative-ai": "^0.21.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": "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-libsql": "^0.1.28",
|
||||||
@@ -71,15 +72,15 @@
|
|||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
|
"epub": "^1.3.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"html2canvas": "^1.4.1",
|
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"officeparser": "^4.1.1",
|
"officeparser": "^4.1.1",
|
||||||
"tokenx": "^0.4.1",
|
"tokenx": "^0.4.1",
|
||||||
"webdav": "4.11.4"
|
"webdav": "4.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.24.3",
|
"@anthropic-ai/sdk": "^0.38.0",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
@@ -87,12 +88,14 @@
|
|||||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||||
"@llm-tools/embedjs-loader-image": "^0.1.28",
|
"@llm-tools/embedjs-loader-image": "^0.1.28",
|
||||||
"@reduxjs/toolkit": "^2.2.5",
|
"@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/adm-zip": "^0",
|
||||||
"@types/fs-extra": "^11",
|
"@types/fs-extra": "^11",
|
||||||
"@types/lodash": "^4.17.5",
|
"@types/lodash": "^4.17.5",
|
||||||
"@types/markdown-it": "^14",
|
"@types/markdown-it": "^14",
|
||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^18.19.9",
|
"@types/node": "^18.19.9",
|
||||||
|
"@types/pako": "^1.0.2",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
"antd": "^5.22.5",
|
"antd": "^5.22.5",
|
||||||
"applescript": "^1.0.0",
|
"applescript": "^1.0.0",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
|
"babel-plugin-styled-components": "^2.1.4",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
@@ -118,6 +122,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.0.0",
|
"eslint-plugin-unused-imports": "^4.0.0",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
@@ -136,7 +141,7 @@
|
|||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-mathjax": "^6.0.0",
|
"rehype-mathjax": "^7.0.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
@@ -160,5 +165,5 @@
|
|||||||
"@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",
|
"@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"
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
|||||||
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||||
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||||
|
export const thirdPartyApplicationExts = ['.draftsExport']
|
||||||
|
export const bookExts = ['.epub']
|
||||||
export const textExts = [
|
export const textExts = [
|
||||||
'.txt', // 普通文本文件
|
'.txt', // 普通文本文件
|
||||||
'.md', // Markdown 文件
|
'.md', // Markdown 文件
|
||||||
@@ -17,6 +19,8 @@ export const textExts = [
|
|||||||
'.ini', // 配置文件
|
'.ini', // 配置文件
|
||||||
'.log', // 日志文件
|
'.log', // 日志文件
|
||||||
'.rtf', // 富文本格式文件
|
'.rtf', // 富文本格式文件
|
||||||
|
'.org', // org-mode 文件
|
||||||
|
'.wiki', // VimWiki 文件
|
||||||
'.tex', // LaTeX 文件
|
'.tex', // LaTeX 文件
|
||||||
'.srt', // 字幕文件
|
'.srt', // 字幕文件
|
||||||
'.xhtml', // XHTML 文件
|
'.xhtml', // XHTML 文件
|
||||||
@@ -33,6 +37,7 @@ export const textExts = [
|
|||||||
'.bat', // Windows 批处理文件
|
'.bat', // Windows 批处理文件
|
||||||
'.sh', // Unix/Linux Shell 脚本文件
|
'.sh', // Unix/Linux Shell 脚本文件
|
||||||
'.py', // Python 脚本文件
|
'.py', // Python 脚本文件
|
||||||
|
'.ipynb', // Jupyter 笔记本格式
|
||||||
'.rb', // Ruby 脚本文件
|
'.rb', // Ruby 脚本文件
|
||||||
'.pl', // Perl 脚本文件
|
'.pl', // Perl 脚本文件
|
||||||
'.sql', // SQL 脚本文件
|
'.sql', // SQL 脚本文件
|
||||||
@@ -88,7 +93,16 @@ export const textExts = [
|
|||||||
'.groovy', // Gradle 构建文件
|
'.groovy', // Gradle 构建文件
|
||||||
'.kts', // Kotlin Script 文件
|
'.kts', // Kotlin Script 文件
|
||||||
'.java', // Java 代码文件
|
'.java', // Java 代码文件
|
||||||
'.cs' // C# 代码文件
|
'.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 = [
|
export const ZOOM_SHORTCUTS = [
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function syncRecursively(target: any, template: any): boolean {
|
|||||||
|
|
||||||
function syncTranslations() {
|
function syncTranslations() {
|
||||||
if (!fs.existsSync(baseFilePath)) {
|
if (!fs.existsSync(baseFilePath)) {
|
||||||
console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名。`)
|
console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,13 +84,13 @@ function syncTranslations() {
|
|||||||
|
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2), 'utf-8')
|
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2) + '\n', 'utf-8')
|
||||||
console.log(`文件 ${file} 已更新同步主模板的内容。`)
|
console.log(`文件 ${file} 已更新同步主模板的内容`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`写入 ${file} 出错:`, error)
|
console.error(`写入 ${file} 出错:`, error)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`文件 ${file} 无需更新。`)
|
console.log(`文件 ${file} 无需更新`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
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 installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
|
|
||||||
import { registerIpc } from './ipc'
|
import { registerIpc } from './ipc'
|
||||||
@@ -20,25 +20,6 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
await updateUserDataPath()
|
await updateUserDataPath()
|
||||||
|
|
||||||
// Register custom protocol
|
|
||||||
if (!app.isDefaultProtocolClient('cherrystudio')) {
|
|
||||||
app.setAsDefaultProtocolClient('cherrystudio')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle protocol open
|
|
||||||
app.on('open-url', (event, url) => {
|
|
||||||
event.preventDefault()
|
|
||||||
const parsedUrl = new URL(url)
|
|
||||||
if (parsedUrl.pathname === 'siliconflow.oauth.login') {
|
|
||||||
const code = parsedUrl.searchParams.get('code')
|
|
||||||
if (code) {
|
|
||||||
// Handle the OAuth code here
|
|
||||||
console.log('OAuth code received:', code)
|
|
||||||
// You can send this code to your renderer process via IPC if needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||||
|
|
||||||
@@ -46,9 +27,8 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
new TrayService()
|
new TrayService()
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
const mainWindow = windowService.getMainWindow()
|
||||||
// dock icon is clicked and there are no other windows open.
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
windowService.createMainWindow()
|
windowService.createMainWindow()
|
||||||
} else {
|
} else {
|
||||||
windowService.showMainWindow()
|
windowService.showMainWindow()
|
||||||
@@ -68,12 +48,7 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
|
|
||||||
// Listen for second instance
|
// Listen for second instance
|
||||||
app.on('second-instance', () => {
|
app.on('second-instance', () => {
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0]
|
windowService.showMainWindow()
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.isMinimized() && mainWindow.restore()
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const backupManager = new BackupManager()
|
|||||||
const exportService = new ExportService(fileManager)
|
const exportService = new ExportService(fileManager)
|
||||||
|
|
||||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
const appUpdater = new AppUpdater(mainWindow)
|
||||||
|
|
||||||
ipcMain.handle('app:info', () => ({
|
ipcMain.handle('app:info', () => ({
|
||||||
version: app.getVersion(),
|
version: app.getVersion(),
|
||||||
@@ -48,6 +48,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle('app:reload', () => mainWindow.reload())
|
ipcMain.handle('app:reload', () => mainWindow.reload())
|
||||||
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
|
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
|
||||||
|
|
||||||
|
// Update
|
||||||
|
ipcMain.handle('app:show-update-dialog', () => appUpdater.showUpdateDialog(mainWindow))
|
||||||
|
|
||||||
// language
|
// language
|
||||||
ipcMain.handle('app:set-language', (_, language) => {
|
ipcMain.handle('app:set-language', (_, language) => {
|
||||||
configManager.setLanguage(language)
|
configManager.setLanguage(language)
|
||||||
@@ -99,9 +102,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
|
|
||||||
// check for update
|
// check for update
|
||||||
ipcMain.handle('app:check-for-update', async () => {
|
ipcMain.handle('app:check-for-update', async () => {
|
||||||
const update = await autoUpdater.checkForUpdates()
|
const update = await appUpdater.autoUpdater.checkForUpdates()
|
||||||
return {
|
return {
|
||||||
currentVersion: autoUpdater.currentVersion,
|
currentVersion: appUpdater.autoUpdater.currentVersion,
|
||||||
updateInfo: update?.updateInfo
|
updateInfo: update?.updateInfo
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
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 || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import * as fs from 'node:fs'
|
import * as fs from 'node:fs'
|
||||||
|
|
||||||
import { LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
||||||
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||||
import { LoaderReturn } from '@shared/config/types'
|
import { LoaderReturn } from '@shared/config/types'
|
||||||
import { FileType, KnowledgeBaseParams } from '@types'
|
import { FileType, KnowledgeBaseParams } from '@types'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
|
import { DraftsExportLoader } from './draftsExportLoader'
|
||||||
|
import { EpubLoader } from './epubLoader'
|
||||||
import { OdLoader, OdType } from './odLoader'
|
import { OdLoader, OdType } from './odLoader'
|
||||||
|
|
||||||
// embedjs内置loader类型
|
// embedjs内置loader类型
|
||||||
const commonExts = ['.pdf', '.csv', '.json', '.docx', '.pptx', '.xlsx', '.md']
|
const commonExts = ['.pdf', '.csv', '.docx', '.pptx', '.xlsx', '.md']
|
||||||
|
|
||||||
export async function addOdLoader(
|
export async function addOdLoader(
|
||||||
ragApplication: RAGApplication,
|
ragApplication: RAGApplication,
|
||||||
@@ -69,8 +72,77 @@ export async function addFileLoader(
|
|||||||
} as LoaderReturn
|
} 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')
|
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(
|
const loaderReturn = await ragApplication.addLoader(
|
||||||
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||||
forceReload
|
forceReload
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import { UpdateInfo } from 'builder-util-runtime'
|
||||||
import { app, BrowserWindow, dialog } from 'electron'
|
import { app, BrowserWindow, dialog } from 'electron'
|
||||||
import logger from 'electron-log'
|
import logger from 'electron-log'
|
||||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
|
||||||
|
|
||||||
import icon from '../../../build/icon.png?asset'
|
import icon from '../../../build/icon.png?asset'
|
||||||
|
|
||||||
export default class AppUpdater {
|
export default class AppUpdater {
|
||||||
autoUpdater: _AppUpdater = autoUpdater
|
autoUpdater: _AppUpdater = autoUpdater
|
||||||
|
private releaseInfo: UpdateInfo | undefined
|
||||||
|
|
||||||
constructor(mainWindow: BrowserWindow) {
|
constructor(mainWindow: BrowserWindow) {
|
||||||
logger.transports.file.level = 'info'
|
logger.transports.file.level = 'info'
|
||||||
@@ -37,34 +39,40 @@ export default class AppUpdater {
|
|||||||
|
|
||||||
// 当需要更新的内容下载完成后
|
// 当需要更新的内容下载完成后
|
||||||
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
||||||
mainWindow.webContents.send('update-downloaded')
|
mainWindow.webContents.send('update-downloaded', releaseInfo)
|
||||||
|
this.releaseInfo = releaseInfo
|
||||||
logger.info('下载完成,询问用户是否更新', releaseInfo)
|
logger.info('下载完成', releaseInfo)
|
||||||
|
|
||||||
dialog
|
|
||||||
.showMessageBox({
|
|
||||||
type: 'info',
|
|
||||||
title: '安装更新',
|
|
||||||
icon,
|
|
||||||
message: `新版本 ${releaseInfo.version} 已准备就绪`,
|
|
||||||
detail: this.formatReleaseNotes(releaseInfo.releaseNotes),
|
|
||||||
buttons: ['稍后安装', '立即安装'],
|
|
||||||
defaultId: 1,
|
|
||||||
cancelId: 0
|
|
||||||
})
|
|
||||||
.then(({ response }) => {
|
|
||||||
if (response === 1) {
|
|
||||||
app.isQuitting = true
|
|
||||||
setImmediate(() => autoUpdater.quitAndInstall())
|
|
||||||
} else {
|
|
||||||
mainWindow.webContents.send('update-downloaded-cancelled')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.autoUpdater = autoUpdater
|
this.autoUpdater = autoUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async showUpdateDialog(mainWindow: BrowserWindow) {
|
||||||
|
if (!this.releaseInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog
|
||||||
|
.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: '安装更新',
|
||||||
|
icon,
|
||||||
|
message: `新版本 ${this.releaseInfo.version} 已准备就绪`,
|
||||||
|
detail: this.formatReleaseNotes(this.releaseInfo.releaseNotes),
|
||||||
|
buttons: ['稍后安装', '立即安装'],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 0
|
||||||
|
})
|
||||||
|
.then(({ response }) => {
|
||||||
|
if (response === 1) {
|
||||||
|
app.isQuitting = true
|
||||||
|
setImmediate(() => autoUpdater.quitAndInstall())
|
||||||
|
} else {
|
||||||
|
mainWindow.webContents.send('update-downloaded-cancelled')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
||||||
if (!releaseNotes) {
|
if (!releaseNotes) {
|
||||||
return '暂无更新说明'
|
return '暂无更新说明'
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { WebDavConfig } from '@types'
|
import { WebDavConfig } from '@types'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
|
import { exec } from 'child_process'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
import * as fs from 'fs-extra'
|
import * as fs from 'fs-extra'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
import WebDav from './WebDav'
|
import WebDav from './WebDav'
|
||||||
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
class BackupManager {
|
class BackupManager {
|
||||||
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
||||||
@@ -18,23 +20,92 @@ class BackupManager {
|
|||||||
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
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(
|
async backup(
|
||||||
_: Electron.IpcMainInvokeEvent,
|
_: Electron.IpcMainInvokeEvent,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
data: string,
|
data: string,
|
||||||
destinationPath: string = this.backupDir
|
destinationPath: string = this.backupDir
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const mainWindow = windowService.getMainWindow()
|
||||||
|
|
||||||
|
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
|
||||||
|
mainWindow?.webContents.send('backup-progress', processData)
|
||||||
|
Logger.log('[BackupManager] backup progress', processData)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.ensureDir(this.tempDir)
|
await fs.ensureDir(this.tempDir)
|
||||||
|
onProgress({ stage: 'preparing', progress: 0, total: 100 })
|
||||||
|
|
||||||
// 将 data 写入临时文件
|
// 将 data 写入临时文件
|
||||||
const tempDataPath = path.join(this.tempDir, 'data.json')
|
const tempDataPath = path.join(this.tempDir, 'data.json')
|
||||||
await fs.writeFile(tempDataPath, data)
|
await fs.writeFile(tempDataPath, data)
|
||||||
|
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
|
||||||
|
|
||||||
// 复制 Data 目录到临时目录
|
// 复制 Data 目录到临时目录
|
||||||
const sourcePath = path.join(app.getPath('userData'), 'Data')
|
const sourcePath = path.join(app.getPath('userData'), 'Data')
|
||||||
const tempDataDir = path.join(this.tempDir, 'Data')
|
const tempDataDir = path.join(this.tempDir, 'Data')
|
||||||
await fs.copy(sourcePath, tempDataDir)
|
|
||||||
|
// 获取源目录总大小
|
||||||
|
const totalSize = await this.getDirSize(sourcePath)
|
||||||
|
let copiedSize = 0
|
||||||
|
|
||||||
|
// 使用流式复制
|
||||||
|
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
|
||||||
|
copiedSize += size
|
||||||
|
const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60))
|
||||||
|
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.setWritableRecursive(tempDataDir)
|
||||||
|
onProgress({ stage: 'compressing', progress: 80, total: 100 })
|
||||||
|
|
||||||
// 使用 adm-zip 创建压缩文件
|
// 使用 adm-zip 创建压缩文件
|
||||||
const zip = new AdmZip()
|
const zip = new AdmZip()
|
||||||
@@ -44,6 +115,7 @@ class BackupManager {
|
|||||||
|
|
||||||
// 清理临时目录
|
// 清理临时目录
|
||||||
await fs.remove(this.tempDir)
|
await fs.remove(this.tempDir)
|
||||||
|
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
||||||
|
|
||||||
Logger.log('Backup completed successfully')
|
Logger.log('Backup completed successfully')
|
||||||
return backupedFilePath
|
return backupedFilePath
|
||||||
@@ -54,34 +126,54 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
||||||
|
const mainWindow = windowService.getMainWindow()
|
||||||
|
|
||||||
|
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
|
||||||
|
mainWindow?.webContents.send('restore-progress', processData)
|
||||||
|
Logger.log('[BackupManager] restore progress', processData)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建临时目录
|
// 创建临时目录
|
||||||
await fs.ensureDir(this.tempDir)
|
await fs.ensureDir(this.tempDir)
|
||||||
|
onProgress({ stage: 'preparing', progress: 0, total: 100 })
|
||||||
|
|
||||||
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
|
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
|
||||||
|
|
||||||
// 使用 adm-zip 解压
|
// 使用 adm-zip 解压
|
||||||
const zip = new AdmZip(backupPath)
|
const zip = new AdmZip(backupPath)
|
||||||
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
|
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
|
||||||
|
onProgress({ stage: 'extracting', progress: 20, total: 100 })
|
||||||
|
|
||||||
Logger.log('[backup] step 2: read data.json')
|
Logger.log('[backup] step 2: read data.json')
|
||||||
|
|
||||||
// 读取 data.json
|
// 读取 data.json
|
||||||
const dataPath = path.join(this.tempDir, 'data.json')
|
const dataPath = path.join(this.tempDir, 'data.json')
|
||||||
const data = await fs.readFile(dataPath, 'utf-8')
|
const data = await fs.readFile(dataPath, 'utf-8')
|
||||||
|
onProgress({ stage: 'reading_data', progress: 40, total: 100 })
|
||||||
|
|
||||||
Logger.log('[backup] step 3: restore Data directory')
|
Logger.log('[backup] step 3: restore Data directory')
|
||||||
|
|
||||||
// 恢复 Data 目录
|
// 恢复 Data 目录
|
||||||
const sourcePath = path.join(this.tempDir, 'Data')
|
const sourcePath = path.join(this.tempDir, 'Data')
|
||||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||||
|
|
||||||
|
// 获取源目录总大小
|
||||||
|
const totalSize = await this.getDirSize(sourcePath)
|
||||||
|
let copiedSize = 0
|
||||||
|
|
||||||
|
await this.setWritableRecursive(destPath)
|
||||||
await fs.remove(destPath)
|
await fs.remove(destPath)
|
||||||
await fs.copy(sourcePath, destPath)
|
|
||||||
|
// 使用流式复制
|
||||||
|
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
|
||||||
|
copiedSize += size
|
||||||
|
const progress = Math.min(90, 40 + Math.floor((copiedSize / totalSize) * 50))
|
||||||
|
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||||
|
})
|
||||||
|
|
||||||
Logger.log('[backup] step 4: clean up temp directory')
|
Logger.log('[backup] step 4: clean up temp directory')
|
||||||
|
|
||||||
// 清理临时目录
|
// 清理临时目录
|
||||||
|
await this.setWritableRecursive(this.tempDir)
|
||||||
await fs.remove(this.tempDir)
|
await fs.remove(this.tempDir)
|
||||||
|
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
||||||
|
|
||||||
Logger.log('[backup] step 5: Restore completed successfully')
|
Logger.log('[backup] step 5: Restore completed successfully')
|
||||||
|
|
||||||
@@ -116,6 +208,44 @@ class BackupManager {
|
|||||||
|
|
||||||
return await this.restore(_, backupedFilePath)
|
return await this.restore(_, backupedFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getDirSize(dirPath: string): Promise<number> {
|
||||||
|
let size = 0
|
||||||
|
const items = await fs.readdir(dirPath, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = path.join(dirPath, item.name)
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
size += await this.getDirSize(fullPath)
|
||||||
|
} else {
|
||||||
|
const stats = await fs.stat(fullPath)
|
||||||
|
size += stats.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyDirWithProgress(
|
||||||
|
source: string,
|
||||||
|
destination: string,
|
||||||
|
onProgress: (size: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
const items = await fs.readdir(source, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const sourcePath = path.join(source, item.name)
|
||||||
|
const destPath = path.join(destination, item.name)
|
||||||
|
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
await fs.ensureDir(destPath)
|
||||||
|
await this.copyDirWithProgress(sourcePath, destPath, onProgress)
|
||||||
|
} else {
|
||||||
|
const stats = await fs.stat(sourcePath)
|
||||||
|
await fs.copy(sourcePath, destPath)
|
||||||
|
onProgress(stats.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BackupManager
|
export default BackupManager
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
// ExportService
|
// ExportService
|
||||||
|
|
||||||
import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, ShadingType, TextRun } from 'docx'
|
import {
|
||||||
|
AlignmentType,
|
||||||
|
BorderStyle,
|
||||||
|
Document,
|
||||||
|
ExternalHyperlink,
|
||||||
|
HeadingLevel,
|
||||||
|
Packer,
|
||||||
|
Paragraph,
|
||||||
|
ShadingType,
|
||||||
|
Table,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
TextRun,
|
||||||
|
VerticalAlign,
|
||||||
|
WidthType
|
||||||
|
} from 'docx'
|
||||||
import { dialog } from 'electron'
|
import { dialog } from 'electron'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
@@ -21,13 +36,54 @@ export class ExportService {
|
|||||||
const tokens = this.md.parse(markdown, {})
|
const tokens = this.md.parse(markdown, {})
|
||||||
const elements: any[] = []
|
const elements: any[] = []
|
||||||
let listLevel = 0
|
let listLevel = 0
|
||||||
|
let currentTable: Table | null = null
|
||||||
|
let currentRowCells: TableCell[] = []
|
||||||
|
let isHeaderRow = false
|
||||||
|
let tableColumnCount = 0
|
||||||
|
let tableRows: TableRow[] = [] // Store rows temporarily
|
||||||
|
|
||||||
const processInlineTokens = (tokens: any[]): TextRun[] => {
|
const processInlineTokens = (tokens: any[], isHeaderRow: boolean): (TextRun | ExternalHyperlink)[] => {
|
||||||
const runs: TextRun[] = []
|
const runs: (TextRun | ExternalHyperlink)[] = []
|
||||||
for (const token of tokens) {
|
let linkText = ''
|
||||||
|
let linkUrl = ''
|
||||||
|
let insideLink = false
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i]
|
||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
|
case 'link_open':
|
||||||
|
insideLink = true
|
||||||
|
linkUrl = token.attrs.find((attr: [string, string]) => attr[0] === 'href')[1]
|
||||||
|
linkText = tokens[i + 1].content
|
||||||
|
i += 1
|
||||||
|
break
|
||||||
|
case 'link_close':
|
||||||
|
if (insideLink && linkUrl && linkText) {
|
||||||
|
// Handle any accumulated link text with the ExternalHyperlink
|
||||||
|
runs.push(
|
||||||
|
new ExternalHyperlink({
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: linkText,
|
||||||
|
style: 'Hyperlink',
|
||||||
|
color: '0000FF',
|
||||||
|
underline: {
|
||||||
|
type: 'single'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
link: linkUrl
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reset link variables
|
||||||
|
linkText = ''
|
||||||
|
linkUrl = ''
|
||||||
|
insideLink = false
|
||||||
|
}
|
||||||
|
break
|
||||||
case 'text':
|
case 'text':
|
||||||
runs.push(new TextRun(token.content))
|
runs.push(new TextRun({ text: token.content, bold: isHeaderRow ? true : false }))
|
||||||
break
|
break
|
||||||
case 'strong':
|
case 'strong':
|
||||||
runs.push(new TextRun({ text: token.content, bold: true }))
|
runs.push(new TextRun({ text: token.content, bold: true }))
|
||||||
@@ -45,7 +101,6 @@ export class ExportService {
|
|||||||
|
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
const token = tokens[i]
|
const token = tokens[i]
|
||||||
|
|
||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case 'heading_open':
|
case 'heading_open':
|
||||||
// 获取标题级别 (h1 -> h6)
|
// 获取标题级别 (h1 -> h6)
|
||||||
@@ -68,7 +123,7 @@ export class ExportService {
|
|||||||
const inlineTokens = tokens[i + 1].children || []
|
const inlineTokens = tokens[i + 1].children || []
|
||||||
elements.push(
|
elements.push(
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
children: processInlineTokens(inlineTokens),
|
children: processInlineTokens(inlineTokens, false),
|
||||||
spacing: {
|
spacing: {
|
||||||
before: 120,
|
before: 120,
|
||||||
after: 120
|
after: 120
|
||||||
@@ -93,7 +148,7 @@ export class ExportService {
|
|||||||
children: [
|
children: [
|
||||||
new TextRun({ text: '•', bold: true }),
|
new TextRun({ text: '•', bold: true }),
|
||||||
new TextRun({ text: '\t' }),
|
new TextRun({ text: '\t' }),
|
||||||
...processInlineTokens(itemInlineTokens)
|
...processInlineTokens(itemInlineTokens, false)
|
||||||
],
|
],
|
||||||
indent: {
|
indent: {
|
||||||
left: listLevel * 720
|
left: listLevel * 720
|
||||||
@@ -171,6 +226,116 @@ export class ExportService {
|
|||||||
)
|
)
|
||||||
i += 3
|
i += 3
|
||||||
break
|
break
|
||||||
|
|
||||||
|
// 表格处理
|
||||||
|
case 'table_open':
|
||||||
|
tableRows = [] // Reset table rows for new table
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'thead_open':
|
||||||
|
isHeaderRow = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tbody_open':
|
||||||
|
isHeaderRow = false
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tr_open':
|
||||||
|
currentRowCells = []
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tr_close':
|
||||||
|
const row = new TableRow({
|
||||||
|
children: currentRowCells,
|
||||||
|
tableHeader: isHeaderRow
|
||||||
|
})
|
||||||
|
tableRows.push(row)
|
||||||
|
// 计算表格有多少列(针对第一行)
|
||||||
|
if (tableColumnCount === 0) {
|
||||||
|
tableColumnCount = currentRowCells.length
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'th_open':
|
||||||
|
case 'td_open':
|
||||||
|
const isFirstColumn = currentRowCells.length === 0 // 判断是否是第一列
|
||||||
|
const borders = {
|
||||||
|
top: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
bottom: isHeaderRow
|
||||||
|
? {
|
||||||
|
style: BorderStyle.SINGLE,
|
||||||
|
size: 0.5,
|
||||||
|
color: '000000'
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cellContent = tokens[i + 1]
|
||||||
|
const cellOptions = {
|
||||||
|
children: [
|
||||||
|
new Paragraph({
|
||||||
|
children: cellContent.children
|
||||||
|
? processInlineTokens(cellContent.children, isHeaderRow || isFirstColumn)
|
||||||
|
: [new TextRun({ text: cellContent.content || '', bold: isHeaderRow || isFirstColumn })],
|
||||||
|
alignment: AlignmentType.CENTER
|
||||||
|
})
|
||||||
|
],
|
||||||
|
verticalAlign: VerticalAlign.CENTER,
|
||||||
|
borders: borders
|
||||||
|
}
|
||||||
|
currentRowCells.push(new TableCell(cellOptions))
|
||||||
|
i += 2 // 跳过内容和结束标记
|
||||||
|
break
|
||||||
|
case 'table_close':
|
||||||
|
// Create table with the collected rows - avoid using protected properties
|
||||||
|
// Create the table with all rows
|
||||||
|
currentTable = new Table({
|
||||||
|
width: {
|
||||||
|
size: 100,
|
||||||
|
type: WidthType.PERCENTAGE
|
||||||
|
},
|
||||||
|
rows: tableRows,
|
||||||
|
borders: {
|
||||||
|
top: {
|
||||||
|
style: BorderStyle.SINGLE,
|
||||||
|
size: 1,
|
||||||
|
color: '000000'
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
style: BorderStyle.SINGLE,
|
||||||
|
size: 1,
|
||||||
|
color: '000000'
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
insideHorizontal: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
insideVertical: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
elements.push(currentTable)
|
||||||
|
currentTable = null
|
||||||
|
tableColumnCount = 0
|
||||||
|
tableRows = []
|
||||||
|
currentRowCells = []
|
||||||
|
isHeaderRow = false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Knowledge Service - Manages knowledge bases using RAG (Retrieval-Augmented Generation)
|
||||||
|
*
|
||||||
|
* This service handles creation, management, and querying of knowledge bases from various sources
|
||||||
|
* including files, directories, URLs, sitemaps, and notes.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Concurrent task processing with workload management
|
||||||
|
* - Multiple data source support
|
||||||
|
* - Vector database integration
|
||||||
|
*
|
||||||
|
* For detailed documentation, see:
|
||||||
|
* @see {@link ../../../docs/technical/KnowledgeService.md}
|
||||||
|
*/
|
||||||
|
|
||||||
import * as fs from 'node:fs'
|
import * as fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
@@ -8,15 +23,77 @@ import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
|
|||||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||||
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
||||||
import { addFileLoader } from '@main/loader'
|
import { addFileLoader } from '@main/loader'
|
||||||
|
import { windowService } from '@main/services/WindowService'
|
||||||
import { getInstanceName } from '@main/utils'
|
import { getInstanceName } from '@main/utils'
|
||||||
import { getAllFiles } from '@main/utils/file'
|
import { getAllFiles } from '@main/utils/file'
|
||||||
import type { LoaderReturn } from '@shared/config/types'
|
import type { LoaderReturn } from '@shared/config/types'
|
||||||
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
|
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
|
import Logger from 'electron-log'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export interface KnowledgeBaseAddItemOptions {
|
||||||
|
base: KnowledgeBaseParams
|
||||||
|
item: KnowledgeItem
|
||||||
|
forceReload?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KnowledgeBaseAddItemOptionsNonNullableAttribute {
|
||||||
|
base: KnowledgeBaseParams
|
||||||
|
item: KnowledgeItem
|
||||||
|
forceReload: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EvaluateTaskWorkload {
|
||||||
|
workload: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoaderDoneReturn = LoaderReturn | null
|
||||||
|
|
||||||
|
enum LoaderTaskItemState {
|
||||||
|
PENDING,
|
||||||
|
PROCESSING,
|
||||||
|
DONE
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoaderTaskItem {
|
||||||
|
state: LoaderTaskItemState
|
||||||
|
task: () => Promise<unknown>
|
||||||
|
evaluateTaskWorkload: EvaluateTaskWorkload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoaderTask {
|
||||||
|
loaderTasks: LoaderTaskItem[]
|
||||||
|
loaderDoneReturn: LoaderDoneReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoaderTaskOfSet {
|
||||||
|
loaderTasks: Set<LoaderTaskItem>
|
||||||
|
loaderDoneReturn: LoaderDoneReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueTaskItem {
|
||||||
|
taskPromise: () => Promise<unknown>
|
||||||
|
resolve: () => void
|
||||||
|
evaluateTaskWorkload: EvaluateTaskWorkload
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaderTaskIntoOfSet = (loaderTask: LoaderTask): LoaderTaskOfSet => {
|
||||||
|
return {
|
||||||
|
loaderTasks: new Set(loaderTask.loaderTasks),
|
||||||
|
loaderDoneReturn: loaderTask.loaderDoneReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class KnowledgeService {
|
class KnowledgeService {
|
||||||
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
|
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
|
||||||
|
// Byte based
|
||||||
|
private workload = 0
|
||||||
|
private processingItemCount = 0
|
||||||
|
private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map()
|
||||||
|
private static MAXIMUM_WORKLOAD = 1024 * 1024 * 80
|
||||||
|
private static MAXIMUM_PROCESSING_ITEM_COUNT = 30
|
||||||
|
private static ERROR_LOADER_RETURN: LoaderReturn = { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' }
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initStorageDir()
|
this.initStorageDir()
|
||||||
@@ -77,79 +154,312 @@ class KnowledgeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public add = async (
|
private maximumLoad() {
|
||||||
_: Electron.IpcMainInvokeEvent,
|
return (
|
||||||
{ base, item, forceReload = false }: { base: KnowledgeBaseParams; item: KnowledgeItem; forceReload: boolean }
|
this.processingItemCount >= KnowledgeService.MAXIMUM_PROCESSING_ITEM_COUNT ||
|
||||||
): Promise<LoaderReturn> => {
|
this.workload >= KnowledgeService.MAXIMUM_WORKLOAD
|
||||||
const ragApplication = await this.getRagApplication(base)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (item.type === 'directory') {
|
private fileTask(
|
||||||
const directory = item.content as string
|
ragApplication: RAGApplication,
|
||||||
const files = getAllFiles(directory)
|
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
|
||||||
const loaderPromises = files.map((file) => addFileLoader(ragApplication, file, base, forceReload))
|
): LoaderTask {
|
||||||
const loaderResults = await Promise.all(loaderPromises)
|
const { base, item, forceReload } = options
|
||||||
const uniqueIds = loaderResults.map((result) => result.uniqueId)
|
const file = item.content as FileType
|
||||||
return {
|
|
||||||
entriesAdded: loaderResults.length,
|
const loaderTask: LoaderTask = {
|
||||||
uniqueId: `DirectoryLoader_${uuidv4()}`,
|
loaderTasks: [
|
||||||
uniqueIds,
|
{
|
||||||
loaderType: 'DirectoryLoader'
|
state: LoaderTaskItemState.PENDING,
|
||||||
} as LoaderReturn
|
task: () =>
|
||||||
|
addFileLoader(ragApplication, file, base, forceReload)
|
||||||
|
.then((result) => {
|
||||||
|
loaderTask.loaderDoneReturn = result
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.error(err)
|
||||||
|
return KnowledgeService.ERROR_LOADER_RETURN
|
||||||
|
}),
|
||||||
|
evaluateTaskWorkload: { workload: file.size }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
loaderDoneReturn: null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'url') {
|
return loaderTask
|
||||||
const content = item.content as string
|
}
|
||||||
if (content.startsWith('http')) {
|
|
||||||
const loaderReturn = await ragApplication.addLoader(
|
private directoryTask(
|
||||||
new WebLoader({ urlOrContent: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
ragApplication: RAGApplication,
|
||||||
forceReload
|
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
|
||||||
)
|
): LoaderTask {
|
||||||
return {
|
const { base, item, forceReload } = options
|
||||||
entriesAdded: loaderReturn.entriesAdded,
|
const directory = item.content as string
|
||||||
uniqueId: loaderReturn.uniqueId,
|
const files = getAllFiles(directory)
|
||||||
uniqueIds: [loaderReturn.uniqueId],
|
const totalFiles = files.length
|
||||||
loaderType: loaderReturn.loaderType
|
let processedFiles = 0
|
||||||
} as LoaderReturn
|
|
||||||
|
const sendDirectoryProcessingPercent = (totalFiles: number, processedFiles: number) => {
|
||||||
|
const mainWindow = windowService.getMainWindow()
|
||||||
|
mainWindow?.webContents.send('directory-processing-percent', {
|
||||||
|
itemId: item.id,
|
||||||
|
percent: (processedFiles / totalFiles) * 100
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaderDoneReturn: LoaderDoneReturn = {
|
||||||
|
entriesAdded: 0,
|
||||||
|
uniqueId: `DirectoryLoader_${uuidv4()}`,
|
||||||
|
uniqueIds: [],
|
||||||
|
loaderType: 'DirectoryLoader'
|
||||||
|
}
|
||||||
|
const loaderTasks: LoaderTaskItem[] = []
|
||||||
|
for (const file of files) {
|
||||||
|
loaderTasks.push({
|
||||||
|
state: LoaderTaskItemState.PENDING,
|
||||||
|
task: () =>
|
||||||
|
addFileLoader(ragApplication, file, base, forceReload)
|
||||||
|
.then((result) => {
|
||||||
|
loaderDoneReturn.entriesAdded += 1
|
||||||
|
processedFiles += 1
|
||||||
|
sendDirectoryProcessingPercent(totalFiles, processedFiles)
|
||||||
|
loaderDoneReturn.uniqueIds.push(result.uniqueId)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.error(err)
|
||||||
|
return KnowledgeService.ERROR_LOADER_RETURN
|
||||||
|
}),
|
||||||
|
evaluateTaskWorkload: { workload: file.size }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loaderTasks,
|
||||||
|
loaderDoneReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private urlTask(
|
||||||
|
ragApplication: RAGApplication,
|
||||||
|
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
|
||||||
|
): LoaderTask {
|
||||||
|
const { base, item, forceReload } = options
|
||||||
|
const content = item.content as string
|
||||||
|
|
||||||
|
const loaderTask: LoaderTask = {
|
||||||
|
loaderTasks: [
|
||||||
|
{
|
||||||
|
state: LoaderTaskItemState.PENDING,
|
||||||
|
task: () => {
|
||||||
|
const loaderReturn = ragApplication.addLoader(
|
||||||
|
new WebLoader({
|
||||||
|
urlOrContent: content,
|
||||||
|
chunkSize: base.chunkSize,
|
||||||
|
chunkOverlap: base.chunkOverlap
|
||||||
|
}),
|
||||||
|
forceReload
|
||||||
|
) as Promise<LoaderReturn>
|
||||||
|
|
||||||
|
return loaderReturn
|
||||||
|
.then((result) => {
|
||||||
|
const { entriesAdded, uniqueId, loaderType } = result
|
||||||
|
loaderTask.loaderDoneReturn = {
|
||||||
|
entriesAdded: entriesAdded,
|
||||||
|
uniqueId: uniqueId,
|
||||||
|
uniqueIds: [uniqueId],
|
||||||
|
loaderType: loaderType
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.error(err)
|
||||||
|
return KnowledgeService.ERROR_LOADER_RETURN
|
||||||
|
})
|
||||||
|
},
|
||||||
|
evaluateTaskWorkload: { workload: 1024 * 1024 * 2 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
loaderDoneReturn: null
|
||||||
|
}
|
||||||
|
return loaderTask
|
||||||
|
}
|
||||||
|
|
||||||
|
private sitemapTask(
|
||||||
|
ragApplication: RAGApplication,
|
||||||
|
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
|
||||||
|
): LoaderTask {
|
||||||
|
const { base, item, forceReload } = options
|
||||||
|
const content = item.content as string
|
||||||
|
|
||||||
|
const loaderTask: LoaderTask = {
|
||||||
|
loaderTasks: [
|
||||||
|
{
|
||||||
|
state: LoaderTaskItemState.PENDING,
|
||||||
|
task: () =>
|
||||||
|
ragApplication
|
||||||
|
.addLoader(
|
||||||
|
new SitemapLoader({ url: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||||
|
forceReload
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
const { entriesAdded, uniqueId, loaderType } = result
|
||||||
|
loaderTask.loaderDoneReturn = {
|
||||||
|
entriesAdded: entriesAdded,
|
||||||
|
uniqueId: uniqueId,
|
||||||
|
uniqueIds: [uniqueId],
|
||||||
|
loaderType: loaderType
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.error(err)
|
||||||
|
return KnowledgeService.ERROR_LOADER_RETURN
|
||||||
|
}),
|
||||||
|
evaluateTaskWorkload: { workload: 1024 * 1024 * 20 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
loaderDoneReturn: null
|
||||||
|
}
|
||||||
|
return loaderTask
|
||||||
|
}
|
||||||
|
|
||||||
|
private noteTask(
|
||||||
|
ragApplication: RAGApplication,
|
||||||
|
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
|
||||||
|
): LoaderTask {
|
||||||
|
const { base, item, forceReload } = options
|
||||||
|
const content = item.content as string
|
||||||
|
console.debug('chunkSize', base.chunkSize)
|
||||||
|
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const contentBytes = encoder.encode(content)
|
||||||
|
const loaderTask: LoaderTask = {
|
||||||
|
loaderTasks: [
|
||||||
|
{
|
||||||
|
state: LoaderTaskItemState.PENDING,
|
||||||
|
task: () => {
|
||||||
|
const loaderReturn = ragApplication.addLoader(
|
||||||
|
new TextLoader({ text: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
|
||||||
|
forceReload
|
||||||
|
) as Promise<LoaderReturn>
|
||||||
|
|
||||||
|
return loaderReturn
|
||||||
|
.then(({ entriesAdded, uniqueId, loaderType }) => {
|
||||||
|
loaderTask.loaderDoneReturn = {
|
||||||
|
entriesAdded: entriesAdded,
|
||||||
|
uniqueId: uniqueId,
|
||||||
|
uniqueIds: [uniqueId],
|
||||||
|
loaderType: loaderType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.error(err)
|
||||||
|
return KnowledgeService.ERROR_LOADER_RETURN
|
||||||
|
})
|
||||||
|
},
|
||||||
|
evaluateTaskWorkload: { workload: contentBytes.length }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
loaderDoneReturn: null
|
||||||
|
}
|
||||||
|
return loaderTask
|
||||||
|
}
|
||||||
|
|
||||||
|
private processingQueueHandle() {
|
||||||
|
const getSubtasksUntilMaximumLoad = (): QueueTaskItem[] => {
|
||||||
|
const queueTaskList: QueueTaskItem[] = []
|
||||||
|
that: for (const [task, resolve] of this.knowledgeItemProcessingQueueMappingPromise) {
|
||||||
|
for (const item of task.loaderTasks) {
|
||||||
|
if (this.maximumLoad()) {
|
||||||
|
break that
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state, task: taskPromise, evaluateTaskWorkload } = item
|
||||||
|
|
||||||
|
if (state !== LoaderTaskItemState.PENDING) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workload } = evaluateTaskWorkload
|
||||||
|
this.workload += workload
|
||||||
|
this.processingItemCount += 1
|
||||||
|
item.state = LoaderTaskItemState.PROCESSING
|
||||||
|
queueTaskList.push({
|
||||||
|
taskPromise: () =>
|
||||||
|
taskPromise().then(() => {
|
||||||
|
this.workload -= workload
|
||||||
|
this.processingItemCount -= 1
|
||||||
|
task.loaderTasks.delete(item)
|
||||||
|
if (task.loaderTasks.size === 0) {
|
||||||
|
this.knowledgeItemProcessingQueueMappingPromise.delete(task)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
this.processingQueueHandle()
|
||||||
|
}),
|
||||||
|
resolve: () => {},
|
||||||
|
evaluateTaskWorkload
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return queueTaskList
|
||||||
}
|
}
|
||||||
|
const subTasks = getSubtasksUntilMaximumLoad()
|
||||||
if (item.type === 'sitemap') {
|
if (subTasks.length > 0) {
|
||||||
const content = item.content as string
|
const subTaskPromises = subTasks.map(({ taskPromise }) => taskPromise())
|
||||||
// @ts-ignore loader type
|
Promise.all(subTaskPromises).then(() => {
|
||||||
const loaderReturn = await ragApplication.addLoader(
|
subTasks.forEach(({ resolve }) => resolve())
|
||||||
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') {
|
private appendProcessingQueue(task: LoaderTask): Promise<LoaderReturn> {
|
||||||
const content = item.content as string
|
return new Promise((resolve) => {
|
||||||
console.debug('chunkSize', base.chunkSize)
|
this.knowledgeItemProcessingQueueMappingPromise.set(loaderTaskIntoOfSet(task), () => {
|
||||||
const loaderReturn = await ragApplication.addLoader(
|
resolve(task.loaderDoneReturn!)
|
||||||
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') {
|
public add = (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
|
||||||
const file = item.content as FileType
|
return new Promise((resolve) => {
|
||||||
|
const { base, item, forceReload = false } = options
|
||||||
|
const optionsNonNullableAttribute = { base, item, forceReload }
|
||||||
|
this.getRagApplication(base)
|
||||||
|
.then((ragApplication) => {
|
||||||
|
const task = (() => {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'file':
|
||||||
|
return this.fileTask(ragApplication, optionsNonNullableAttribute)
|
||||||
|
case 'directory':
|
||||||
|
return this.directoryTask(ragApplication, optionsNonNullableAttribute)
|
||||||
|
case 'url':
|
||||||
|
return this.urlTask(ragApplication, optionsNonNullableAttribute)
|
||||||
|
case 'sitemap':
|
||||||
|
return this.sitemapTask(ragApplication, optionsNonNullableAttribute)
|
||||||
|
case 'note':
|
||||||
|
return this.noteTask(ragApplication, optionsNonNullableAttribute)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
return await addFileLoader(ragApplication, file, base, forceReload)
|
if (task) {
|
||||||
}
|
this.appendProcessingQueue(task).then(() => {
|
||||||
|
resolve(task.loaderDoneReturn!)
|
||||||
return { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' }
|
})
|
||||||
|
this.processingQueueHandle()
|
||||||
|
} else {
|
||||||
|
resolve(KnowledgeService.ERROR_LOADER_RETURN)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.error(err)
|
||||||
|
resolve(KnowledgeService.ERROR_LOADER_RETURN)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove = async (
|
public remove = async (
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ function getShortcutHandler(shortcut: Shortcut) {
|
|||||||
case 'show_app':
|
case 'show_app':
|
||||||
return (window: BrowserWindow) => {
|
return (window: BrowserWindow) => {
|
||||||
if (window.isVisible()) {
|
if (window.isVisible()) {
|
||||||
window.hide()
|
if (window.isFocused()) {
|
||||||
|
window.hide()
|
||||||
|
} else {
|
||||||
|
window.focus()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.show()
|
window.show()
|
||||||
window.focus()
|
window.focus()
|
||||||
@@ -43,8 +47,8 @@ function formatShortcutKey(shortcut: string[]): string {
|
|||||||
|
|
||||||
function handleZoom(delta: number) {
|
function handleZoom(delta: number) {
|
||||||
return (window: BrowserWindow) => {
|
return (window: BrowserWindow) => {
|
||||||
const currentZoom = window.webContents.getZoomFactor()
|
const currentZoom = configManager.getZoomFactor()
|
||||||
const newZoom = currentZoom + delta
|
const newZoom = Number((currentZoom + delta).toFixed(1))
|
||||||
if (newZoom >= 0.1 && newZoom <= 5.0) {
|
if (newZoom >= 0.1 && newZoom <= 5.0) {
|
||||||
window.webContents.setZoomFactor(newZoom)
|
window.webContents.setZoomFactor(newZoom)
|
||||||
configManager.setZoomFactor(newZoom)
|
configManager.setZoomFactor(newZoom)
|
||||||
@@ -52,8 +56,65 @@ function handleZoom(delta: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
|
||||||
|
shortcut: string | string[]
|
||||||
|
): string => {
|
||||||
|
const accelerator = (() => {
|
||||||
|
if (Array.isArray(shortcut)) {
|
||||||
|
return shortcut
|
||||||
|
} else {
|
||||||
|
return shortcut.split('+').map((key) => key.trim())
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
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('+')
|
||||||
|
}
|
||||||
|
|
||||||
export function registerShortcuts(window: BrowserWindow) {
|
export function registerShortcuts(window: BrowserWindow) {
|
||||||
window.webContents.setZoomFactor(configManager.getZoomFactor())
|
window.once('ready-to-show', () => {
|
||||||
|
window.webContents.setZoomFactor(configManager.getZoomFactor())
|
||||||
|
})
|
||||||
|
|
||||||
const register = () => {
|
const register = () => {
|
||||||
if (window.isDestroyed()) return
|
if (window.isDestroyed()) return
|
||||||
@@ -75,11 +136,11 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
|
|
||||||
const accelerator = formatShortcutKey(shortcut.shortcut)
|
const accelerator = formatShortcutKey(shortcut.shortcut)
|
||||||
|
|
||||||
if (shortcut.key === 'show_app') {
|
if (shortcut.key === 'show_app' && shortcut.enabled) {
|
||||||
showAppAccelerator = accelerator
|
showAppAccelerator = accelerator
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shortcut.key === 'mini_window') {
|
if (shortcut.key === 'mini_window' && shortcut.enabled) {
|
||||||
showMiniWindowAccelerator = accelerator
|
showMiniWindowAccelerator = accelerator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +161,10 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shortcut.enabled) {
|
if (shortcut.enabled) {
|
||||||
globalShortcut.register(formatShortcutKey(shortcut.shortcut), () => handler(window))
|
const accelerator = convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(
|
||||||
|
shortcut.shortcut
|
||||||
|
)
|
||||||
|
globalShortcut.register(accelerator, () => handler(window))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
|
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
|
||||||
@@ -116,12 +180,16 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
|
|
||||||
if (showAppAccelerator) {
|
if (showAppAccelerator) {
|
||||||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||||
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
|
const accelerator =
|
||||||
|
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showAppAccelerator)
|
||||||
|
handler && globalShortcut.register(accelerator, () => handler(window))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showMiniWindowAccelerator) {
|
if (showMiniWindowAccelerator) {
|
||||||
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
||||||
handler && globalShortcut.register(showMiniWindowAccelerator, () => handler(window))
|
const accelerator =
|
||||||
|
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showMiniWindowAccelerator)
|
||||||
|
handler && globalShortcut.register(accelerator, () => handler(window))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('[ShortcutService] Failed to unregister shortcuts')
|
Logger.error('[ShortcutService] Failed to unregister shortcuts')
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export class WindowService {
|
|||||||
|
|
||||||
public createMainWindow(): BrowserWindow {
|
public createMainWindow(): BrowserWindow {
|
||||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.show()
|
||||||
return this.mainWindow
|
return this.mainWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ export class WindowService {
|
|||||||
show: false, // 初始不显示
|
show: false, // 初始不显示
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
transparent: isMac,
|
transparent: isMac,
|
||||||
vibrancy: 'under-window',
|
vibrancy: 'sidebar',
|
||||||
visualEffectState: 'active',
|
visualEffectState: 'active',
|
||||||
titleBarStyle: isLinux ? 'default' : 'hidden',
|
titleBarStyle: isLinux ? 'default' : 'hidden',
|
||||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||||
@@ -149,6 +150,17 @@ export class WindowService {
|
|||||||
this.wasFullScreen = false
|
this.wasFullScreen = false
|
||||||
mainWindow.webContents.send('fullscreen-status-changed', false)
|
mainWindow.webContents.send('fullscreen-status-changed', false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加Escape键退出全屏的支持
|
||||||
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
|
// 当按下Escape键且窗口处于全屏状态时退出全屏
|
||||||
|
if (input.key === 'Escape' && !input.alt && !input.control && !input.meta && !input.shift) {
|
||||||
|
if (mainWindow.isFullScreen()) {
|
||||||
|
event.preventDefault()
|
||||||
|
mainWindow.setFullScreen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
||||||
@@ -240,25 +252,45 @@ export class WindowService {
|
|||||||
return app.quit()
|
return app.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是全屏状态,直接退出
|
// 如果是Windows或Linux,且处于全屏状态,则退出应用
|
||||||
if (this.wasFullScreen) {
|
if (this.wasFullScreen) {
|
||||||
return app.quit()
|
if (isWin || isLinux) {
|
||||||
|
return app.quit()
|
||||||
|
} else {
|
||||||
|
event.preventDefault()
|
||||||
|
mainWindow.setFullScreen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
mainWindow.hide()
|
mainWindow.hide()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
this.mainWindow = null
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('show', () => {
|
||||||
|
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||||
|
this.miniWindow.hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public showMainWindow() {
|
public showMainWindow() {
|
||||||
if (this.mainWindow) {
|
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||||
|
this.miniWindow.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
if (this.mainWindow.isMinimized()) {
|
if (this.mainWindow.isMinimized()) {
|
||||||
return this.mainWindow.restore()
|
this.mainWindow.restore()
|
||||||
}
|
}
|
||||||
this.mainWindow.show()
|
this.mainWindow.show()
|
||||||
this.mainWindow.focus()
|
this.mainWindow.focus()
|
||||||
} else {
|
} else {
|
||||||
this.createMainWindow()
|
this.mainWindow = this.createMainWindow()
|
||||||
|
this.mainWindow.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +301,10 @@ export class WindowService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectionMenuWindow) {
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.hide()
|
||||||
|
}
|
||||||
|
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||||
this.selectionMenuWindow.hide()
|
this.selectionMenuWindow.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export function getAllFiles(dirPath: string, arrayOfFiles: FileType[] = []): Fil
|
|||||||
const files = fs.readdirSync(dirPath)
|
const files = fs.readdirSync(dirPath)
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
|
if (file.startsWith('.')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const fullPath = path.join(dirPath, file)
|
const fullPath = path.join(dirPath, file)
|
||||||
if (fs.statSync(fullPath).isDirectory()) {
|
if (fs.statSync(fullPath).isDirectory()) {
|
||||||
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles)
|
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles)
|
||||||
|
|||||||
4
src/preload/index.d.ts
vendored
@@ -15,6 +15,7 @@ declare global {
|
|||||||
api: {
|
api: {
|
||||||
getAppInfo: () => Promise<AppInfo>
|
getAppInfo: () => Promise<AppInfo>
|
||||||
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
|
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
|
||||||
|
showUpdateDialog: () => Promise<void>
|
||||||
openWebsite: (url: string) => void
|
openWebsite: (url: string) => void
|
||||||
setProxy: (proxy: string | undefined) => void
|
setProxy: (proxy: string | undefined) => void
|
||||||
setLanguage: (theme: LanguageVarious) => void
|
setLanguage: (theme: LanguageVarious) => void
|
||||||
@@ -119,6 +120,9 @@ declare global {
|
|||||||
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
||||||
decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise<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 { electronAPI } from '@electron-toolkit/preload'
|
||||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
@@ -8,6 +8,7 @@ const api = {
|
|||||||
reload: () => ipcRenderer.invoke('app:reload'),
|
reload: () => ipcRenderer.invoke('app:reload'),
|
||||||
setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy),
|
setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy),
|
||||||
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
|
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
|
||||||
|
showUpdateDialog: () => ipcRenderer.invoke('app:show-update-dialog'),
|
||||||
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
|
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
|
||||||
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
|
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
|
||||||
restartTray: () => ipcRenderer.invoke('app:restart-tray'),
|
restartTray: () => ipcRenderer.invoke('app:restart-tray'),
|
||||||
@@ -104,6 +105,9 @@ const api = {
|
|||||||
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
|
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
|
||||||
decrypt: (encryptedData: string, iv: string, secretKey: string) =>
|
decrypt: (encryptedData: string, iv: string, secretKey: string) =>
|
||||||
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
|
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
|
||||||
|
},
|
||||||
|
shell: {
|
||||||
|
openExternal: shell.openExternal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; connect-src 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:" />
|
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>
|
<style>
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import { PersistGate } from 'redux-persist/integration/react'
|
|||||||
import Sidebar from './components/app/Sidebar'
|
import Sidebar from './components/app/Sidebar'
|
||||||
import TopViewContainer from './components/TopView'
|
import TopViewContainer from './components/TopView'
|
||||||
import AntdProvider from './context/AntdProvider'
|
import AntdProvider from './context/AntdProvider'
|
||||||
|
import StyleSheetManager from './context/StyleSheetManager'
|
||||||
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
|
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
|
||||||
import { ThemeProvider } from './context/ThemeProvider'
|
import { ThemeProvider } from './context/ThemeProvider'
|
||||||
|
import NavigationHandler from './handler/NavigationHandler'
|
||||||
import AgentsPage from './pages/agents/AgentsPage'
|
import AgentsPage from './pages/agents/AgentsPage'
|
||||||
import AppsPage from './pages/apps/AppsPage'
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
import FilesPage from './pages/files/FilesPage'
|
import FilesPage from './pages/files/FilesPage'
|
||||||
@@ -22,29 +24,32 @@ import TranslatePage from './pages/translate/TranslatePage'
|
|||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeProvider>
|
<StyleSheetManager>
|
||||||
<AntdProvider>
|
<ThemeProvider>
|
||||||
<SyntaxHighlighterProvider>
|
<AntdProvider>
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
<SyntaxHighlighterProvider>
|
||||||
<TopViewContainer>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<HashRouter>
|
<TopViewContainer>
|
||||||
<Sidebar />
|
<HashRouter>
|
||||||
<Routes>
|
<NavigationHandler />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Sidebar />
|
||||||
<Route path="/agents" element={<AgentsPage />} />
|
<Routes>
|
||||||
<Route path="/paintings" element={<PaintingsPage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/translate" element={<TranslatePage />} />
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
<Route path="/files" element={<FilesPage />} />
|
<Route path="/paintings" element={<PaintingsPage />} />
|
||||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
<Route path="/files" element={<FilesPage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||||
</Routes>
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
</HashRouter>
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
</TopViewContainer>
|
</Routes>
|
||||||
</PersistGate>
|
</HashRouter>
|
||||||
</SyntaxHighlighterProvider>
|
</TopViewContainer>
|
||||||
</AntdProvider>
|
</PersistGate>
|
||||||
</ThemeProvider>
|
</SyntaxHighlighterProvider>
|
||||||
|
</AntdProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</StyleSheetManager>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/renderer/src/assets/images/apps/abacus.webp
Normal file
|
After Width: | Height: | Size: 1.6 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/cici-app-logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/renderer/src/assets/images/apps/cici.webp
Normal file
|
After Width: | Height: | Size: 4.2 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 |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 7.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 |
|
Before Width: | Height: | Size: 3.2 KiB 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 |
|
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/wpslingxi.webp
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/renderer/src/assets/images/apps/xiaoyi.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/renderer/src/assets/images/apps/you.jpg
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/apps/yuanbao.webp
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/renderer/src/assets/images/models/codestral.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 7.5 KiB |
BIN
src/renderer/src/assets/images/models/perplexity.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/renderer/src/assets/images/models/xirang.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/models/xirang_dark.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/providers/DMXAPI.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
src/renderer/src/assets/images/providers/cohere.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
src/renderer/src/assets/images/providers/lmstudio.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/providers/modelscope.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/renderer/src/assets/images/providers/o3.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 6.0 KiB |
BIN
src/renderer/src/assets/images/providers/perplexity.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/providers/tencent-cloud-ti.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/renderer/src/assets/images/providers/xirang.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
14
src/renderer/src/assets/images/search/tavily-dark.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg width="778" height="257" viewBox="0 0 778 257" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M97.1853 5.35901L127.346 53.1064C132.19 60.7745 126.68 70.7725 117.61 70.7725H105.279V142.278H87.4492V-0.00683594C91.1876 -0.00683594 94.926 1.78179 97.1853 5.35901Z" fill="#8FBCFA"/>
|
||||||
|
<path d="M47.5482 53.1064L77.7098 5.35901C79.9691 1.78179 83.7075 -0.00683594 87.4459 -0.00683594V142.279C81.0587 141.981 74.8755 143.829 69.616 147.544V70.7725H57.2849C48.2149 70.7725 42.7047 60.7745 47.5482 53.1064Z" fill="#468BFF"/>
|
||||||
|
<path d="M182.003 189.445L107.34 189.445C111.648 184.622 114.201 178.481 114.476 171.615H252.782C252.782 175.353 250.993 179.092 247.416 181.351L199.669 211.512C192.001 216.356 182.003 210.846 182.003 201.776V189.445Z" fill="#FDBB11"/>
|
||||||
|
<path d="M199.668 131.718L247.415 161.879C250.993 164.138 252.781 167.877 252.781 171.615H114.471C114.72 165.212 112.733 158.898 108.957 153.785H182.002V141.454C182.002 132.384 192 126.874 199.668 131.718Z" fill="#F6D785"/>
|
||||||
|
<path d="M46.9409 209.797L3.37891 253.359C6.02226 256.003 9.93035 257.381 14.0576 256.45L69.1472 244.014C77.9944 242.017 81.1678 231.051 74.7545 224.638L66.035 215.918L98.7916 183.055C105.771 176.075 105.462 164.899 98.6758 158.113L46.9409 209.797Z" fill="#FF9A9D"/>
|
||||||
|
<path d="M40.8221 190.708L73.6898 157.963C80.6694 150.983 91.8931 151.328 98.679 158.113L46.9436 209.802L3.38131 253.364C0.737954 250.721 -0.640662 246.812 0.291 242.685L12.7265 187.596C14.7236 178.748 25.6895 175.575 32.1028 181.988L40.8221 190.708Z" fill="#FE363B"/>
|
||||||
|
<path d="M777.344 93.6689L718.337 234.049H692.704L713.348 186.567L675.156 93.6689H702.166L726.766 160.246L751.711 93.6689H777.344Z" fill="#FFFFFF"/>
|
||||||
|
<path d="M664.096 70.1191V188.976H640.012V70.1191H664.096Z" fill="#FFFFFF"/>
|
||||||
|
<path d="M606.041 82.2736C601.797 82.2736 598.242 80.9547 595.375 78.3168C592.622 75.5643 591.246 72.181 591.246 68.1668C591.246 64.1527 592.622 60.8267 595.375 58.1889C598.242 55.4363 601.797 54.0601 606.041 54.0601C610.284 54.0601 613.783 55.4363 616.535 58.1889C619.402 60.8267 620.836 63.6942 620.836 67.7084C620.836 71.7225 619.402 75.5643 616.535 78.3168C613.783 80.9547 610.284 82.2736 606.041 82.2736ZM617.911 93.6279V188.978H593.827V93.6279H617.911Z" fill="#FFFFFF"/>
|
||||||
|
<path d="M532.3 166.783L556.385 93.6689H582.018L546.751 188.976H517.505L482.41 93.6689H508.215L532.3 166.783Z" fill="#FFFFFF"/>
|
||||||
|
<path d="M371.52 140.972C371.52 131.338 373.412 122.794 377.197 115.339C381.096 107.884 386.314 102.15 392.852 98.1355C399.504 94.1213 406.901 92.1143 415.044 92.1143C422.155 92.1143 428.348 93.5479 433.624 96.4151C439.014 99.2823 443.315 102.895 446.526 107.253V93.6626H470.783V188.969H446.526V175.035C443.43 179.507 439.129 183.235 433.624 186.217C428.233 189.084 421.983 190.518 414.872 190.518C406.844 190.518 399.504 188.453 392.852 184.324C386.314 180.196 381.096 174.404 377.197 166.949C373.412 159.38 371.52 150.72 371.52 140.972ZM446.526 141.316C446.526 135.467 445.379 130.478 443.086 126.349C440.792 122.105 437.695 118.894 433.796 116.715C429.896 114.421 425.71 113.274 421.237 113.274C416.764 113.274 412.636 114.364 408.851 116.543C405.066 118.722 401.97 121.933 399.561 126.177C397.267 130.306 396.12 135.237 396.12 140.972C396.12 146.706 397.267 151.753 399.561 156.111C401.97 160.354 405.066 163.623 408.851 165.917C412.75 168.211 416.879 169.357 421.237 169.357C425.71 169.357 429.896 168.268 433.796 166.089C437.695 163.795 440.792 160.584 443.086 156.455C445.379 152.211 446.526 147.165 446.526 141.316Z" fill="#FFFFFF"/>
|
||||||
|
<path d="M340.767 113.445V159.55C340.767 162.762 341.513 165.113 343.004 166.604C344.609 167.98 347.247 168.668 350.917 168.668H362.099V188.968H346.96C326.66 188.968 316.51 179.105 316.51 159.378V113.445H305.156V93.6614H316.51V70.0928H340.767V93.6614H362.099V113.445H340.767Z" fill="#FFFFFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
14
src/renderer/src/assets/images/search/tavily.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg width="778" height="257" viewBox="0 0 778 257" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M97.1853 5.35901L127.346 53.1064C132.19 60.7745 126.68 70.7725 117.61 70.7725H105.279V142.278H87.4492V-0.00683594C91.1876 -0.00683594 94.926 1.78179 97.1853 5.35901Z" fill="#8FBCFA"/>
|
||||||
|
<path d="M47.5482 53.1064L77.7098 5.35901C79.9691 1.78179 83.7075 -0.00683594 87.4459 -0.00683594V142.279C81.0587 141.981 74.8755 143.829 69.616 147.544V70.7725H57.2849C48.2149 70.7725 42.7047 60.7745 47.5482 53.1064Z" fill="#468BFF"/>
|
||||||
|
<path d="M182.003 189.445L107.34 189.445C111.648 184.622 114.201 178.481 114.476 171.615H252.782C252.782 175.353 250.993 179.092 247.416 181.351L199.669 211.512C192.001 216.356 182.003 210.846 182.003 201.776V189.445Z" fill="#FDBB11"/>
|
||||||
|
<path d="M199.668 131.718L247.415 161.879C250.993 164.138 252.781 167.877 252.781 171.615H114.471C114.72 165.212 112.733 158.898 108.957 153.785H182.002V141.454C182.002 132.384 192 126.874 199.668 131.718Z" fill="#F6D785"/>
|
||||||
|
<path d="M46.9409 209.797L3.37891 253.359C6.02226 256.003 9.93035 257.381 14.0576 256.45L69.1472 244.014C77.9944 242.017 81.1678 231.051 74.7545 224.638L66.035 215.918L98.7916 183.055C105.771 176.075 105.462 164.899 98.6758 158.113L46.9409 209.797Z" fill="#FF9A9D"/>
|
||||||
|
<path d="M40.8221 190.708L73.6898 157.963C80.6694 150.983 91.8931 151.328 98.679 158.113L46.9436 209.802L3.38131 253.364C0.737954 250.721 -0.640662 246.812 0.291 242.685L12.7265 187.596C14.7236 178.748 25.6895 175.575 32.1028 181.988L40.8221 190.708Z" fill="#FE363B"/>
|
||||||
|
<path d="M777.344 93.6689L718.337 234.049H692.704L713.348 186.567L675.156 93.6689H702.166L726.766 160.246L751.711 93.6689H777.344Z" fill="#2C2F32"/>
|
||||||
|
<path d="M664.096 70.1191V188.976H640.012V70.1191H664.096Z" fill="#2C2F32"/>
|
||||||
|
<path d="M606.041 82.2736C601.797 82.2736 598.242 80.9547 595.375 78.3168C592.622 75.5643 591.246 72.181 591.246 68.1668C591.246 64.1527 592.622 60.8267 595.375 58.1889C598.242 55.4363 601.797 54.0601 606.041 54.0601C610.284 54.0601 613.783 55.4363 616.535 58.1889C619.402 60.8267 620.836 63.6942 620.836 67.7084C620.836 71.7225 619.402 75.5643 616.535 78.3168C613.783 80.9547 610.284 82.2736 606.041 82.2736ZM617.911 93.6279V188.978H593.827V93.6279H617.911Z" fill="#2C2F32"/>
|
||||||
|
<path d="M532.3 166.783L556.385 93.6689H582.018L546.751 188.976H517.505L482.41 93.6689H508.215L532.3 166.783Z" fill="#2C2F32"/>
|
||||||
|
<path d="M371.52 140.972C371.52 131.338 373.412 122.794 377.197 115.339C381.096 107.884 386.314 102.15 392.852 98.1355C399.504 94.1213 406.901 92.1143 415.044 92.1143C422.155 92.1143 428.348 93.5479 433.624 96.4151C439.014 99.2823 443.315 102.895 446.526 107.253V93.6626H470.783V188.969H446.526V175.035C443.43 179.507 439.129 183.235 433.624 186.217C428.233 189.084 421.983 190.518 414.872 190.518C406.844 190.518 399.504 188.453 392.852 184.324C386.314 180.196 381.096 174.404 377.197 166.949C373.412 159.38 371.52 150.72 371.52 140.972ZM446.526 141.316C446.526 135.467 445.379 130.478 443.086 126.349C440.792 122.105 437.695 118.894 433.796 116.715C429.896 114.421 425.71 113.274 421.237 113.274C416.764 113.274 412.636 114.364 408.851 116.543C405.066 118.722 401.97 121.933 399.561 126.177C397.267 130.306 396.12 135.237 396.12 140.972C396.12 146.706 397.267 151.753 399.561 156.111C401.97 160.354 405.066 163.623 408.851 165.917C412.75 168.211 416.879 169.357 421.237 169.357C425.71 169.357 429.896 168.268 433.796 166.089C437.695 163.795 440.792 160.584 443.086 156.455C445.379 152.211 446.526 147.165 446.526 141.316Z" fill="#2C2F32"/>
|
||||||
|
<path d="M340.767 113.445V159.55C340.767 162.762 341.513 165.113 343.004 166.604C344.609 167.98 347.247 168.668 350.917 168.668H362.099V188.968H346.96C326.66 188.968 316.51 179.105 316.51 159.378V113.445H305.156V93.6614H316.51V70.0928H340.767V93.6614H362.099V113.445H340.767Z" fill="#2C2F32"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -1,3 +1,5 @@
|
|||||||
|
@use './container.scss';
|
||||||
|
|
||||||
#inputbar {
|
#inputbar {
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
@@ -10,6 +12,10 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tabpane:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-segmented-group {
|
.ant-segmented-group {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/renderer/src/assets/styles/container.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#content-container {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border-top: 0.5px solid var(--color-border);
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-left: 0.5px solid var(--color-border);
|
||||||
|
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
@import './markdown.scss';
|
@use './markdown.scss';
|
||||||
@import './ant.scss';
|
@use './ant.scss';
|
||||||
@import './scrollbar.scss';
|
@use './scrollbar.scss';
|
||||||
|
@use './container.scss';
|
||||||
@import '../fonts/icon-fonts/iconfont.css';
|
@import '../fonts/icon-fonts/iconfont.css';
|
||||||
@import '../fonts/ubuntu/ubuntu.css';
|
@import '../fonts/ubuntu/ubuntu.css';
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ body[theme-mode='light'] {
|
|||||||
--color-background: var(--color-white);
|
--color-background: var(--color-white);
|
||||||
--color-background-soft: var(--color-white-soft);
|
--color-background-soft: var(--color-white-soft);
|
||||||
--color-background-mute: var(--color-white-mute);
|
--color-background-mute: var(--color-white-mute);
|
||||||
--color-background-opacity: rgba(255, 255, 255, 0.7);
|
--color-background-opacity: rgba(235, 235, 235, 0.7);
|
||||||
|
|
||||||
--color-primary: #00b96b;
|
--color-primary: #00b96b;
|
||||||
--color-primary-soft: #00b96b99;
|
--color-primary-soft: #00b96b99;
|
||||||
@@ -176,14 +177,6 @@ body,
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content-container {
|
|
||||||
background-color: var(--color-background);
|
|
||||||
border-top: 0.5px solid var(--color-border);
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-left: 0.5px solid var(--color-border);
|
|
||||||
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -256,6 +249,9 @@ body,
|
|||||||
border: 1px solid var(--color-background-mute);
|
border: 1px solid var(--color-background-mute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.group-menu-bar {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
}
|
||||||
code {
|
code {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,10 @@
|
|||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:has(+ ul) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
|||||||
@@ -14,7 +14,15 @@ const ModelAvatar: FC<Props> = ({ model, size, props }) => {
|
|||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
src={getModelLogo(model?.id || '')}
|
src={getModelLogo(model?.id || '')}
|
||||||
style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
minWidth: size,
|
||||||
|
minHeight: size,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
{...props}>
|
{...props}>
|
||||||
{first(model?.name)}
|
{first(model?.name)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|||||||
@@ -46,23 +46,28 @@ const DragableList: FC<Props<any>> = ({
|
|||||||
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
||||||
<Droppable droppableId="droppable" {...droppableProps}>
|
<Droppable droppableId="droppable" {...droppableProps}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
|
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
|
||||||
{list.map((item, index) => {
|
{list.map((item, index) => {
|
||||||
const id = item.id || item
|
const id = item.id || item
|
||||||
return (
|
return (
|
||||||
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index} {...droppableProps}>
|
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div
|
<div
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
|
style={{
|
||||||
|
...listStyle,
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
marginBottom: 8
|
||||||
|
}}>
|
||||||
{children(item, index)}
|
{children(item, index)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
|
|||||||
35
src/renderer/src/components/Ellipsis/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
import styled, { css } from 'styled-components'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
maxLine?: number
|
||||||
|
} & HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
const Ellipsis = (props: Props) => {
|
||||||
|
const { maxLine = 1, children, ...rest } = props
|
||||||
|
return (
|
||||||
|
<EllipsisContainer $maxLine={maxLine} {...rest}>
|
||||||
|
{children}
|
||||||
|
</EllipsisContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiLineEllipsis = css<{ $maxLine: number }>`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: ${({ $maxLine }) => $maxLine};
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
`
|
||||||
|
|
||||||
|
const singleLineEllipsis = css`
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const EllipsisContainer = styled.div<{ $maxLine: number }>`
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
${({ $maxLine }) => ($maxLine > 1 ? multiLineEllipsis : singleLineEllipsis)}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default Ellipsis
|
||||||
134
src/renderer/src/components/Icons/FallbackFavicon.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
// FallbackFavicon component that tries multiple favicon sources
|
||||||
|
interface FallbackFaviconProps {
|
||||||
|
hostname: string
|
||||||
|
alt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FallbackFavicon: React.FC<FallbackFaviconProps> = ({ hostname, alt }) => {
|
||||||
|
type FaviconState =
|
||||||
|
| { status: 'idle' }
|
||||||
|
| { status: 'loading' }
|
||||||
|
| { status: 'failed' }
|
||||||
|
| { status: 'loaded'; src: string }
|
||||||
|
|
||||||
|
const [faviconState, setFaviconState] = useState<FaviconState>({ status: 'idle' })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset state when hostname changes
|
||||||
|
setFaviconState({ status: 'loading' })
|
||||||
|
|
||||||
|
// Generate all possible favicon URLs
|
||||||
|
const faviconUrls = [
|
||||||
|
`https://favicon.splitbee.io/?url=${hostname}`,
|
||||||
|
`https://${hostname}/favicon.ico`,
|
||||||
|
`https://icon.horse/icon/${hostname}`,
|
||||||
|
`https://favicon.cccyun.cc/${hostname}`,
|
||||||
|
`https://favicon.im/${hostname}`,
|
||||||
|
`https://www.google.com/s2/favicons?domain=${hostname}`
|
||||||
|
]
|
||||||
|
|
||||||
|
// Main controller to abort all requests when needed
|
||||||
|
const controller = new AbortController()
|
||||||
|
const { signal } = controller
|
||||||
|
|
||||||
|
// Create a promise for each favicon URL
|
||||||
|
const faviconPromises = faviconUrls.map((url) =>
|
||||||
|
fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
signal,
|
||||||
|
credentials: 'omit'
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to fetch ${url}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// Rethrow aborted errors but silence other failures
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
console.debug(`Failed to fetch favicon from ${url}:`, error)
|
||||||
|
return null // Return null for failed requests
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a timeout promise
|
||||||
|
const timeoutPromise = new Promise<string>((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
resolve(faviconUrls[0]) // Default to first URL after timeout
|
||||||
|
}, 2000)
|
||||||
|
|
||||||
|
// Clear timeout if signal is aborted
|
||||||
|
signal.addEventListener('abort', () => clearTimeout(timer))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use Promise.race to get the first successful result
|
||||||
|
Promise.race([
|
||||||
|
// Filter out failed requests (null results)
|
||||||
|
Promise.any(faviconPromises)
|
||||||
|
.then((result) => result || faviconUrls[0]) // Ensure we always have a string, not null
|
||||||
|
.catch(() => faviconUrls[0]),
|
||||||
|
timeoutPromise
|
||||||
|
])
|
||||||
|
.then((url) => {
|
||||||
|
setFaviconState({ status: 'loaded', src: url })
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.debug('All favicon requests failed:', error)
|
||||||
|
setFaviconState({ status: 'loaded', src: faviconUrls[0] })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
controller.abort()
|
||||||
|
}
|
||||||
|
}, [hostname]) // Only depend on hostname
|
||||||
|
|
||||||
|
const handleError = () => {
|
||||||
|
setFaviconState({ status: 'failed' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render based on current state
|
||||||
|
if (faviconState.status === 'failed') {
|
||||||
|
return <FaviconPlaceholder>{hostname.charAt(0).toUpperCase()}</FaviconPlaceholder>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faviconState.status === 'loaded') {
|
||||||
|
return <Favicon src={faviconState.src} alt={alt} onError={handleError} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FaviconLoading />
|
||||||
|
}
|
||||||
|
|
||||||
|
const FaviconLoading = styled.div`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
`
|
||||||
|
|
||||||
|
const FaviconPlaceholder = styled.div`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--color-primary-1);
|
||||||
|
color: var(--color-primary-6);
|
||||||
|
font-size: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
`
|
||||||
|
const Favicon = styled.img`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
`
|
||||||
|
|
||||||
|
export default FallbackFavicon
|
||||||
@@ -24,6 +24,7 @@ const MinAppIcon: FC<Props> = ({ app, size = 48, style }) => {
|
|||||||
width: `${size}px`,
|
width: `${size}px`,
|
||||||
height: `${size}px`,
|
height: `${size}px`,
|
||||||
backgroundColor: _app.background,
|
backgroundColor: _app.background,
|
||||||
|
...app.style,
|
||||||
...style
|
...style
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
17
src/renderer/src/components/Icons/UnWrapIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const UnWrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
className="unwrap_svg__lucide unwrap_svg__lucide-text unwrap_svg__size-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}>
|
||||||
|
<path d="M17 6.1H3M21 12.1H3M15.1 18H3" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
export default UnWrapIcon
|
||||||
20
src/renderer/src/components/Icons/WrapIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const WrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
className="wrap_svg__lucide wrap_svg__lucide-wrap-text wrap_svg__size-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}>
|
||||||
|
<path d="M3 6h18M3 12h15a3 3 0 1 1 0 6h-4" />
|
||||||
|
<path d="m16 16-2 2 2 2M3 18h7" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
export default WrapIcon
|
||||||
63
src/renderer/src/components/MarkdownShadowDOMRenderer.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { StyleProvider } from '@ant-design/cssinjs'
|
||||||
|
import React, { useEffect, useRef } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { StyleSheetManager } from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShadowDOMRenderer: React.FC<Props> = ({ children }) => {
|
||||||
|
const hostRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [shadowRoot, setShadowRoot] = React.useState<ShadowRoot | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const host = hostRef.current
|
||||||
|
if (!host) return
|
||||||
|
|
||||||
|
// 创建 shadow root
|
||||||
|
const shadow = host.shadowRoot || host.attachShadow({ mode: 'open' })
|
||||||
|
|
||||||
|
// 获取原始样式表
|
||||||
|
const markdownStyleSheet = Array.from(document.styleSheets).find((sheet) => {
|
||||||
|
try {
|
||||||
|
return Array.from(sheet.cssRules).some((rule: CSSRule) => {
|
||||||
|
return rule.cssText?.includes('.markdown')
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (markdownStyleSheet) {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
const cssRules = Array.from(markdownStyleSheet.cssRules)
|
||||||
|
.map((rule) => rule.cssText)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
style.textContent = cssRules
|
||||||
|
shadow.appendChild(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
setShadowRoot(shadow)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!shadowRoot) {
|
||||||
|
return <div ref={hostRef} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={hostRef}>
|
||||||
|
{createPortal(
|
||||||
|
<StyleSheetManager target={shadowRoot}>
|
||||||
|
<StyleProvider container={shadowRoot} layer>
|
||||||
|
{children}
|
||||||
|
</StyleProvider>
|
||||||
|
</StyleSheetManager>,
|
||||||
|
shadowRoot
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShadowDOMRenderer
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable react/no-unknown-property */
|
/* eslint-disable react/no-unknown-property */
|
||||||
import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { CloseOutlined, CodeOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import { isMac, isWindows } from '@renderer/config/constant'
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
|
import { AppLogo } from '@renderer/config/env'
|
||||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||||
import { useBridge } from '@renderer/hooks/useBridge'
|
import { useBridge } from '@renderer/hooks/useBridge'
|
||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
@@ -41,7 +42,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MinApp.onClose = onClose
|
MinApp.onClose = onClose
|
||||||
|
const openDevTools = () => {
|
||||||
|
if (webviewRef.current) {
|
||||||
|
webviewRef.current.openDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
const onReload = () => {
|
const onReload = () => {
|
||||||
if (webviewRef.current) {
|
if (webviewRef.current) {
|
||||||
webviewRef.current.src = app.url
|
webviewRef.current.src = app.url
|
||||||
@@ -49,14 +54,17 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onOpenLink = () => {
|
const onOpenLink = () => {
|
||||||
window.api.openWebsite(app.url)
|
if (webviewRef.current) {
|
||||||
|
const currentUrl = webviewRef.current.getURL()
|
||||||
|
window.api.openWebsite(currentUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTogglePin = () => {
|
const onTogglePin = () => {
|
||||||
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
|
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
|
||||||
updatePinnedMinapps(newPinned)
|
updatePinnedMinapps(newPinned)
|
||||||
}
|
}
|
||||||
|
const isInDevelopment = process.env.NODE_ENV === 'development'
|
||||||
const Title = () => {
|
const Title = () => {
|
||||||
return (
|
return (
|
||||||
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
||||||
@@ -75,6 +83,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
<ExportOutlined />
|
<ExportOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{isInDevelopment && (
|
||||||
|
<Button onClick={openDevTools}>
|
||||||
|
<CodeOutlined />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button onClick={() => onClose()}>
|
<Button onClick={() => onClose()}>
|
||||||
<CloseOutlined />
|
<CloseOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -236,6 +249,10 @@ export default class MinApp {
|
|||||||
await delay(0)
|
await delay(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!app.logo) {
|
||||||
|
app.logo = AppLogo
|
||||||
|
}
|
||||||
|
|
||||||
MinApp.app = app
|
MinApp.app = app
|
||||||
store.dispatch(setMinappShow(true))
|
store.dispatch(setMinappShow(true))
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
allowClear
|
allowClear
|
||||||
autoFocus
|
autoFocus
|
||||||
style={{ paddingLeft: 0 }}
|
style={{ paddingLeft: 0 }}
|
||||||
bordered={false}
|
variant="borderless"
|
||||||
size="middle"
|
size="middle"
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
100
src/renderer/src/components/Popups/BackupPopup.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { backup } from '@renderer/services/BackupService'
|
||||||
|
import { Modal, Progress } from 'antd'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { TopView } from '../TopView'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressData {
|
||||||
|
stage: string
|
||||||
|
progress: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const [progressData, setProgressData] = useState<ProgressData>()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const removeListener = window.electron.ipcRenderer.on('backup-progress', (_, data: ProgressData) => {
|
||||||
|
setProgressData(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeListener()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onOk = async () => {
|
||||||
|
await backup()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resolve({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProgressText = () => {
|
||||||
|
if (!progressData) return ''
|
||||||
|
|
||||||
|
if (progressData.stage === 'copying_files') {
|
||||||
|
return t(`backup.progress.${progressData.stage}`, {
|
||||||
|
progress: Math.floor(progressData.progress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return t(`backup.progress.${progressData.stage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupPopup.hide = onCancel
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('backup.title')}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
transitionName="ant-move-down"
|
||||||
|
okText={t('backup.confirm.button')}
|
||||||
|
centered>
|
||||||
|
{!progressData && <div>{t('backup.content')}</div>}
|
||||||
|
{progressData && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
||||||
|
<Progress percent={Math.floor(progressData.progress)} strokeColor="var(--color-primary)" />
|
||||||
|
<div style={{ marginTop: 16 }}>{getProgressText()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopViewKey = 'BackupPopup'
|
||||||
|
|
||||||
|
export default class BackupPopup {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}
|
||||||
|
static show() {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PopupContainer
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
TopViewKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import App from '@renderer/pages/apps/App'
|
|||||||
import { Popover } from 'antd'
|
import { Popover } from 'antd'
|
||||||
import { Empty } from 'antd'
|
import { Empty } from 'antd'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@@ -26,8 +26,22 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 100)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setMaxHeight(window.innerHeight - 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<PopoverContent>
|
<PopoverContent maxHeight={maxHeight}>
|
||||||
<AppsContainer>
|
<AppsContainer>
|
||||||
{minapps.map((app) => (
|
{minapps.map((app) => (
|
||||||
<App key={app.id} app={app} onClick={handleClose} size={50} />
|
<App key={app.id} app={app} onClick={handleClose} size={50} />
|
||||||
@@ -54,11 +68,14 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopoverContent = styled(Scrollbar)``
|
const PopoverContent = styled(Scrollbar)<{ maxHeight: number }>`
|
||||||
|
max-height: ${(props) => props.maxHeight}px;
|
||||||
|
overflow-y: auto;
|
||||||
|
`
|
||||||
|
|
||||||
const AppsContainer = styled.div`
|
const AppsContainer = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, minmax(90px, 1fr));
|
grid-template-columns: repeat(8, minmax(90px, 1fr));
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Input, Modal } from 'antd'
|
import { Input, Modal } from 'antd'
|
||||||
import { TextAreaProps } from 'antd/es/input'
|
import { TextAreaProps } from 'antd/es/input'
|
||||||
import { useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Box } from '../Layout'
|
import { Box } from '../Layout'
|
||||||
import { TopView } from '../TopView'
|
import { TopView } from '../TopView'
|
||||||
@@ -27,6 +27,7 @@ const PromptPopupContainer: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = useState(defaultValue)
|
const [value, setValue] = useState(defaultValue)
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
|
const textAreaRef = useRef<any>(null)
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@@ -41,17 +42,35 @@ const PromptPopupContainer: React.FC<Props> = ({
|
|||||||
resolve(null)
|
resolve(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAfterOpenChange = (visible: boolean) => {
|
||||||
|
if (visible) {
|
||||||
|
const textArea = textAreaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
textArea.focus()
|
||||||
|
const length = textArea.value.length
|
||||||
|
textArea.setSelectionRange(length, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PromptPopup.hide = onCancel
|
PromptPopup.hide = onCancel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose} centered>
|
<Modal
|
||||||
|
title={title}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
afterOpenChange={handleAfterOpenChange}
|
||||||
|
centered>
|
||||||
<Box mb={8}>{message}</Box>
|
<Box mb={8}>{message}</Box>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
ref={textAreaRef}
|
||||||
placeholder={inputPlaceholder}
|
placeholder={inputPlaceholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
allowClear
|
allowClear
|
||||||
autoFocus
|
|
||||||
onPressEnter={onOk}
|
onPressEnter={onOk}
|
||||||
rows={1}
|
rows={1}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
|||||||
100
src/renderer/src/components/Popups/RestorePopup.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { restore } from '@renderer/services/BackupService'
|
||||||
|
import { Modal, Progress } from 'antd'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { TopView } from '../TopView'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressData {
|
||||||
|
stage: string
|
||||||
|
progress: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const [progressData, setProgressData] = useState<ProgressData>()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const removeListener = window.electron.ipcRenderer.on('restore-progress', (_, data: ProgressData) => {
|
||||||
|
setProgressData(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeListener()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onOk = async () => {
|
||||||
|
await restore()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resolve({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProgressText = () => {
|
||||||
|
if (!progressData) return ''
|
||||||
|
|
||||||
|
if (progressData.stage === 'copying_files') {
|
||||||
|
return t(`restore.progress.${progressData.stage}`, {
|
||||||
|
progress: Math.floor(progressData.progress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return t(`restore.progress.${progressData.stage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
RestorePopup.hide = onCancel
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('restore.title')}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
transitionName="ant-move-down"
|
||||||
|
okText={t('restore.confirm.button')}
|
||||||
|
centered>
|
||||||
|
{!progressData && <div>{t('restore.content')}</div>}
|
||||||
|
{progressData && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
||||||
|
<Progress percent={Math.floor(progressData.progress)} strokeColor="var(--color-primary)" />
|
||||||
|
<div style={{ marginTop: 16 }}>{getProgressText()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopViewKey = 'RestorePopup'
|
||||||
|
|
||||||
|
export default class RestorePopup {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}
|
||||||
|
static show() {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PopupContainer
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
TopViewKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
|||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
|
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
|
||||||
import { first, sortBy } from 'lodash'
|
import { first, sortBy } from 'lodash'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [keyboardSelectedId, setKeyboardSelectedId] = useState<string>('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPinnedModels = async () => {
|
const loadPinnedModels = async () => {
|
||||||
@@ -62,41 +63,59 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
setPinnedModels(sortBy(newPinnedModels, ['group', 'name']))
|
setPinnedModels(sortBy(newPinnedModels, ['group', 'name']))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据输入的文本筛选模型
|
||||||
|
const getFilteredModels = useCallback(
|
||||||
|
(provider) => {
|
||||||
|
const nonEmbeddingModels = provider.models.filter((m) => !isEmbeddingModel(m))
|
||||||
|
|
||||||
|
if (!searchText.trim()) {
|
||||||
|
return sortBy(nonEmbeddingModels, ['group', 'name'])
|
||||||
|
}
|
||||||
|
|
||||||
|
const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean)
|
||||||
|
|
||||||
|
return sortBy(nonEmbeddingModels, ['group', 'name']).filter((m) => {
|
||||||
|
const fullName = provider.isSystem
|
||||||
|
? `${m.name}${m.provider}${t('provider.' + provider.id)}`
|
||||||
|
: `${m.name}${m.provider}`
|
||||||
|
|
||||||
|
const lowerFullName = fullName.toLowerCase()
|
||||||
|
return keywords.every((keyword) => lowerFullName.includes(keyword))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[searchText, t]
|
||||||
|
)
|
||||||
|
|
||||||
const filteredItems: MenuItem[] = providers
|
const filteredItems: MenuItem[] = providers
|
||||||
.filter((p) => p.models && p.models.length > 0)
|
.filter((p) => p.models && p.models.length > 0)
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
const filteredModels = sortBy(p.models, ['group', 'name'])
|
const filteredModels = getFilteredModels(p).map((m) => ({
|
||||||
.filter((m) => !isEmbeddingModel(m))
|
key: getModelUniqId(m),
|
||||||
.filter((m) =>
|
label: (
|
||||||
[m.name + m.provider + t('provider.' + p.id)].join('').toLowerCase().includes(searchText.toLowerCase())
|
<ModelItem>
|
||||||
)
|
<ModelNameRow>
|
||||||
.map((m) => ({
|
<span>{m?.name}</span> <ModelTags model={m} />
|
||||||
key: getModelUniqId(m),
|
</ModelNameRow>
|
||||||
label: (
|
<PinIcon
|
||||||
<ModelItem>
|
onClick={(e) => {
|
||||||
<ModelNameRow>
|
e.stopPropagation()
|
||||||
<span>{m?.name}</span> <ModelTags model={m} />
|
togglePin(getModelUniqId(m))
|
||||||
</ModelNameRow>
|
}}
|
||||||
<PinIcon
|
isPinned={pinnedModels.includes(getModelUniqId(m))}>
|
||||||
onClick={(e) => {
|
<PushpinOutlined />
|
||||||
e.stopPropagation()
|
</PinIcon>
|
||||||
togglePin(getModelUniqId(m))
|
</ModelItem>
|
||||||
}}
|
),
|
||||||
isPinned={pinnedModels.includes(getModelUniqId(m))}>
|
icon: (
|
||||||
<PushpinOutlined />
|
<Avatar src={getModelLogo(m?.id || '')} size={24}>
|
||||||
</PinIcon>
|
{first(m?.name)}
|
||||||
</ModelItem>
|
</Avatar>
|
||||||
),
|
),
|
||||||
icon: (
|
onClick: () => {
|
||||||
<Avatar src={getModelLogo(m?.id || '')} size={24}>
|
resolve(m)
|
||||||
{first(m?.name)}
|
setOpen(false)
|
||||||
</Avatar>
|
}
|
||||||
),
|
}))
|
||||||
onClick: () => {
|
|
||||||
resolve(m)
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Only return the group if it has filtered models
|
// Only return the group if it has filtered models
|
||||||
return filteredModels.length > 0
|
return filteredModels.length > 0
|
||||||
@@ -153,10 +172,12 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
setKeyboardSelectedId('')
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = async () => {
|
const onClose = async () => {
|
||||||
|
setKeyboardSelectedId('')
|
||||||
resolve(undefined)
|
resolve(undefined)
|
||||||
SelectModelPopup.hide()
|
SelectModelPopup.hide()
|
||||||
}
|
}
|
||||||
@@ -176,6 +197,85 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
}
|
}
|
||||||
}, [open, model])
|
}, [open, model])
|
||||||
|
|
||||||
|
// 获取所有可见的模型项
|
||||||
|
const getVisibleModelItems = useCallback(() => {
|
||||||
|
const items: { key: string; model: Model }[] = []
|
||||||
|
|
||||||
|
// 如果有置顶模型且没有搜索文本,添加置顶模型
|
||||||
|
if (pinnedModels.length > 0 && searchText.length === 0) {
|
||||||
|
providers
|
||||||
|
.flatMap((p) => p.models || [])
|
||||||
|
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
||||||
|
.forEach((m) => items.push({ key: getModelUniqId(m) + '_pinned', model: m }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加其他过滤后的模型
|
||||||
|
providers.forEach((p) => {
|
||||||
|
if (p.models) {
|
||||||
|
getFilteredModels(p).forEach((m) => {
|
||||||
|
const modelId = getModelUniqId(m)
|
||||||
|
const isPinned = pinnedModels.includes(modelId)
|
||||||
|
// 如果是搜索状态,或者不是固定模型,才添加到列表中
|
||||||
|
if (searchText.length > 0 || !isPinned) {
|
||||||
|
items.push({
|
||||||
|
key: isPinned ? modelId + '_pinned' : modelId,
|
||||||
|
model: m
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}, [pinnedModels, searchText, providers, getFilteredModels])
|
||||||
|
|
||||||
|
// 处理键盘导航
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
const items = getVisibleModelItems()
|
||||||
|
if (items.length === 0) return
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
const currentIndex = items.findIndex((item) => item.key === keyboardSelectedId)
|
||||||
|
let nextIndex
|
||||||
|
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
nextIndex = e.key === 'ArrowDown' ? 0 : items.length - 1
|
||||||
|
} else {
|
||||||
|
nextIndex =
|
||||||
|
e.key === 'ArrowDown' ? (currentIndex + 1) % items.length : (currentIndex - 1 + items.length) % items.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextItem = items[nextIndex]
|
||||||
|
setKeyboardSelectedId(nextItem.key)
|
||||||
|
|
||||||
|
const element = document.querySelector(`[data-menu-id="${nextItem.key}"]`)
|
||||||
|
element?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault() // 阻止回车的默认行为
|
||||||
|
if (keyboardSelectedId) {
|
||||||
|
const selectedItem = items.find((item) => item.key === keyboardSelectedId)
|
||||||
|
if (selectedItem) {
|
||||||
|
resolve(selectedItem.model)
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[keyboardSelectedId, getVisibleModelItems, resolve, setOpen]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [handleKeyDown])
|
||||||
|
|
||||||
|
// 搜索文本改变时重置键盘选中状态
|
||||||
|
useEffect(() => {
|
||||||
|
setKeyboardSelectedId('')
|
||||||
|
}, [searchText])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
@@ -208,20 +308,21 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
allowClear
|
allowClear
|
||||||
autoFocus
|
autoFocus
|
||||||
style={{ paddingLeft: 0 }}
|
style={{ paddingLeft: 0 }}
|
||||||
bordered={false}
|
variant="borderless"
|
||||||
size="middle"
|
size="middle"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// 防止上下键移动光标
|
||||||
|
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
||||||
<Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
|
<Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
|
||||||
<Container>
|
<Container>
|
||||||
{filteredItems.length > 0 ? (
|
{filteredItems.length > 0 ? (
|
||||||
<StyledMenu
|
<StyledMenu items={filteredItems} selectedKeys={[keyboardSelectedId]} mode="inline" inlineIndent={6} />
|
||||||
items={filteredItems}
|
|
||||||
selectedKeys={model ? [getModelUniqId(model)] : []}
|
|
||||||
mode="inline"
|
|
||||||
inlineIndent={6}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
|||||||
setTimeout(resizeTextArea, 0)
|
setTimeout(resizeTextArea, 0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleAfterOpenChange = (visible: boolean) => {
|
||||||
|
if (visible) {
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
textArea.focus()
|
||||||
|
const length = textArea.value.length
|
||||||
|
textArea.setSelectionRange(length, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextEditPopup.hide = onCancel
|
TextEditPopup.hide = onCancel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,6 +76,7 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
|||||||
onOk={onOk}
|
onOk={onOk}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
|
afterOpenChange={handleAfterOpenChange}
|
||||||
centered>
|
centered>
|
||||||
<TextArea
|
<TextArea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
import DefaultAvatar from '@renderer/assets/images/avatar.png'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import ImageStorage from '@renderer/services/ImageStorage'
|
import ImageStorage from '@renderer/services/ImageStorage'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setAvatar } from '@renderer/store/runtime'
|
import { setAvatar } from '@renderer/store/runtime'
|
||||||
import { setUserName } from '@renderer/store/settings'
|
import { setUserName } from '@renderer/store/settings'
|
||||||
import { compressImage } from '@renderer/utils'
|
import { compressImage, isEmoji } from '@renderer/utils'
|
||||||
import { Avatar, Input, Modal, Upload } from 'antd'
|
import { Avatar, Dropdown, Input, Modal, Popover, Upload } from 'antd'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { Center, HStack } from '../Layout'
|
import EmojiPicker from '../EmojiPicker'
|
||||||
|
import { Center, HStack, VStack } from '../Layout'
|
||||||
import { TopView } from '../TopView'
|
import { TopView } from '../TopView'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -19,6 +21,8 @@ interface Props {
|
|||||||
|
|
||||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
|
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false)
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { userName } = useSettings()
|
const { userName } = useSettings()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@@ -36,6 +40,85 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
resolve({})
|
resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEmojiClick = async (emoji: string) => {
|
||||||
|
try {
|
||||||
|
// set emoji string
|
||||||
|
await ImageStorage.set('avatar', emoji)
|
||||||
|
// update avatar display
|
||||||
|
dispatch(setAvatar(emoji))
|
||||||
|
setEmojiPickerOpen(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
window.message.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleReset = async () => {
|
||||||
|
try {
|
||||||
|
await ImageStorage.set('avatar', DefaultAvatar)
|
||||||
|
dispatch(setAvatar(DefaultAvatar))
|
||||||
|
setDropdownOpen(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
window.message.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'upload',
|
||||||
|
label: (
|
||||||
|
<div style={{ width: '100%', textAlign: 'center' }}>
|
||||||
|
<Upload
|
||||||
|
customRequest={() => {}}
|
||||||
|
accept="image/png, image/jpeg, image/gif"
|
||||||
|
itemRender={() => null}
|
||||||
|
maxCount={1}
|
||||||
|
onChange={async ({ file }) => {
|
||||||
|
try {
|
||||||
|
const _file = file.originFileObj as File
|
||||||
|
if (_file.type === 'image/gif') {
|
||||||
|
await ImageStorage.set('avatar', _file)
|
||||||
|
} else {
|
||||||
|
const compressedFile = await compressImage(_file)
|
||||||
|
await ImageStorage.set('avatar', compressedFile)
|
||||||
|
}
|
||||||
|
dispatch(setAvatar(await ImageStorage.get('avatar')))
|
||||||
|
setDropdownOpen(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
window.message.error(error.message)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{t('settings.general.image_upload')}
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'emoji',
|
||||||
|
label: (
|
||||||
|
<div
|
||||||
|
style={{ width: '100%', textAlign: 'center' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setEmojiPickerOpen(true)
|
||||||
|
setDropdownOpen(false)
|
||||||
|
}}>
|
||||||
|
{t('settings.general.emoji_picker')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'reset',
|
||||||
|
label: (
|
||||||
|
<div
|
||||||
|
style={{ width: '100%', textAlign: 'center' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleReset()
|
||||||
|
}}>
|
||||||
|
{t('settings.general.avatar.reset')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
width="300px"
|
width="300px"
|
||||||
@@ -47,29 +130,40 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
transitionName="ant-move-down"
|
transitionName="ant-move-down"
|
||||||
centered>
|
centered>
|
||||||
<Center mt="30px">
|
<Center mt="30px">
|
||||||
<Upload
|
<VStack alignItems="center" gap="10px">
|
||||||
customRequest={() => {}}
|
<Dropdown
|
||||||
accept="image/png, image/jpeg"
|
menu={{ items }}
|
||||||
itemRender={() => null}
|
trigger={['click']}
|
||||||
maxCount={1}
|
open={dropdownOpen}
|
||||||
onChange={async ({ file }) => {
|
align={{ offset: [0, 4] }}
|
||||||
try {
|
placement="bottom"
|
||||||
const _file = file.originFileObj as File
|
onOpenChange={(visible) => {
|
||||||
const compressedFile = await compressImage(_file)
|
setDropdownOpen(visible)
|
||||||
await ImageStorage.set('avatar', compressedFile)
|
if (visible) {
|
||||||
dispatch(setAvatar(await ImageStorage.get('avatar')))
|
setEmojiPickerOpen(false)
|
||||||
} catch (error: any) {
|
}
|
||||||
window.message.error(error.message)
|
}}>
|
||||||
}
|
<Popover
|
||||||
}}>
|
content={<EmojiPicker onEmojiClick={handleEmojiClick} />}
|
||||||
<UserAvatar src={avatar} />
|
trigger="click"
|
||||||
</Upload>
|
open={emojiPickerOpen}
|
||||||
|
onOpenChange={(visible) => {
|
||||||
|
setEmojiPickerOpen(visible)
|
||||||
|
if (visible) {
|
||||||
|
setDropdownOpen(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placement="bottom">
|
||||||
|
{isEmoji(avatar) ? <EmojiAvatar>{avatar}</EmojiAvatar> : <UserAvatar src={avatar} />}
|
||||||
|
</Popover>
|
||||||
|
</Dropdown>
|
||||||
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
<HStack alignItems="center" gap="10px" p="20px">
|
<HStack alignItems="center" gap="10px" p="20px">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('settings.general.user_name.placeholder')}
|
placeholder={t('settings.general.user_name.placeholder')}
|
||||||
value={userName}
|
value={userName}
|
||||||
onChange={(e) => dispatch(setUserName(e.target.value))}
|
onChange={(e) => dispatch(setUserName(e.target.value.trim()))}
|
||||||
style={{ flex: 1, textAlign: 'center', width: '100%' }}
|
style={{ flex: 1, textAlign: 'center', width: '100%' }}
|
||||||
maxLength={30}
|
maxLength={30}
|
||||||
/>
|
/>
|
||||||
@@ -88,6 +182,23 @@ const UserAvatar = styled(Avatar)`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const EmojiAvatar = styled.div`
|
||||||
|
cursor: pointer;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 20%;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40px;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default class UserPopup {
|
export default class UserPopup {
|
||||||
static topviewId = 0
|
static topviewId = 0
|
||||||
static hide() {
|
static hide() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import useAvatar from '@renderer/hooks/useAvatar'
|
|||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { isEmoji } from '@renderer/utils'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { Avatar } from 'antd'
|
import { Avatar } from 'antd'
|
||||||
@@ -50,6 +51,7 @@ const Sidebar: FC = () => {
|
|||||||
|
|
||||||
const onOpenDocs = () => {
|
const onOpenDocs = () => {
|
||||||
MinApp.start({
|
MinApp.start({
|
||||||
|
id: 'docs',
|
||||||
name: t('docs.title'),
|
name: t('docs.title'),
|
||||||
url: 'https://docs.cherry-ai.com/',
|
url: 'https://docs.cherry-ai.com/',
|
||||||
logo: AppLogo
|
logo: AppLogo
|
||||||
@@ -63,7 +65,11 @@ const Sidebar: FC = () => {
|
|||||||
backgroundColor: sidebarBgColor,
|
backgroundColor: sidebarBgColor,
|
||||||
zIndex: minappShow ? 10000 : 'initial'
|
zIndex: minappShow ? 10000 : 'initial'
|
||||||
}}>
|
}}>
|
||||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
{isEmoji(avatar) ? (
|
||||||
|
<EmojiAvatar onClick={onEditUser}>{avatar}</EmojiAvatar>
|
||||||
|
) : (
|
||||||
|
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||||
|
)}
|
||||||
<MainMenusContainer>
|
<MainMenusContainer>
|
||||||
<Menus onClick={MinApp.onClose}>
|
<Menus onClick={MinApp.onClose}>
|
||||||
<MainMenus />
|
<MainMenus />
|
||||||
@@ -77,9 +83,11 @@ const Sidebar: FC = () => {
|
|||||||
</AppsContainer>
|
</AppsContainer>
|
||||||
)}
|
)}
|
||||||
</MainMenusContainer>
|
</MainMenusContainer>
|
||||||
<Menus onClick={MinApp.onClose}>
|
<Menus>
|
||||||
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
|
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
|
||||||
<Icon onClick={onOpenDocs}>
|
<Icon
|
||||||
|
onClick={onOpenDocs}
|
||||||
|
className={minappShow && MinApp.app?.url === 'https://docs.cherry-ai.com/' ? 'active' : ''}>
|
||||||
<QuestionCircleOutlined />
|
<QuestionCircleOutlined />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -93,8 +101,14 @@ const Sidebar: FC = () => {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
<StyledLink
|
||||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
onClick={async () => {
|
||||||
|
if (minappShow) {
|
||||||
|
await MinApp.close()
|
||||||
|
}
|
||||||
|
await to(isLocalAi ? '/settings/assistant' : '/settings/provider')
|
||||||
|
}}>
|
||||||
|
<Icon className={pathname.startsWith('/settings') && !minappShow ? 'active' : ''}>
|
||||||
<i className="iconfont icon-setting" />
|
<i className="iconfont icon-setting" />
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
@@ -108,10 +122,11 @@ const MainMenus: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { sidebarIcons } = useSettings()
|
const { sidebarIcons } = useSettings()
|
||||||
|
const { minappShow } = useRuntime()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '')
|
||||||
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
assistants: <i className="iconfont icon-chat" />,
|
assistants: <i className="iconfont icon-chat" />,
|
||||||
@@ -139,7 +154,13 @@ const MainMenus: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink onClick={() => navigate(path)}>
|
<StyledLink
|
||||||
|
onClick={async () => {
|
||||||
|
if (minappShow) {
|
||||||
|
await MinApp.close()
|
||||||
|
}
|
||||||
|
navigate(path)
|
||||||
|
}}>
|
||||||
<Icon className={isActive}>{iconMap[icon]}</Icon>
|
<Icon className={isActive}>{iconMap[icon]}</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -150,6 +171,7 @@ const MainMenus: FC = () => {
|
|||||||
const PinnedApps: FC = () => {
|
const PinnedApps: FC = () => {
|
||||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { minappShow } = useRuntime()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
||||||
@@ -164,11 +186,12 @@ const PinnedApps: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
const isActive = minappShow && MinApp.app?.id === app.id
|
||||||
return (
|
return (
|
||||||
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink>
|
<StyledLink>
|
||||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||||
<Icon onClick={() => MinApp.start(app)}>
|
<Icon onClick={() => MinApp.start(app)} className={isActive ? 'active' : ''}>
|
||||||
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -202,6 +225,24 @@ const AvatarImg = styled(Avatar)`
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const EmojiAvatar = styled.div`
|
||||||
|
width: 31px;
|
||||||
|
height: 31px;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
margin-bottom: ${isMac ? '12px' : '12px'};
|
||||||
|
margin-top: ${isMac ? '0px' : '2px'};
|
||||||
|
border-radius: 20%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-app-region: none;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
font-size: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
const MainMenusContainer = styled.div`
|
const MainMenusContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const DEFAULT_TEMPERATURE = 1.0
|
|||||||
export const DEFAULT_CONTEXTCOUNT = 5
|
export const DEFAULT_CONTEXTCOUNT = 5
|
||||||
export const DEFAULT_MAX_TOKENS = 4096
|
export const DEFAULT_MAX_TOKENS = 4096
|
||||||
export const DEFAULT_KNOWLEDGE_DOCUMENT_COUNT = 6
|
export const DEFAULT_KNOWLEDGE_DOCUMENT_COUNT = 6
|
||||||
|
export const DEFAULT_KNOWLEDGE_THRESHOLD = 0.0
|
||||||
export const FONT_FAMILY =
|
export const FONT_FAMILY =
|
||||||
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
|
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,22 @@ export const EMBEDDING_MODELS = [
|
|||||||
{
|
{
|
||||||
id: 'text-embedding-004',
|
id: 'text-embedding-004',
|
||||||
max_context: 2048
|
max_context: 2048
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deepset-mxbai-embed-de-large-v1',
|
||||||
|
max_context: 512
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mxbai-embed-large-v1',
|
||||||
|
max_context: 512
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mxbai-embed-2d-large-v1',
|
||||||
|
max_context: 512
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mistral-embed',
|
||||||
|
max_context: 8000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url'
|
import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url'
|
||||||
|
import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url'
|
||||||
import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url'
|
import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url'
|
||||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
|
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
|
||||||
|
import BaiduAiSearchLogo from '@renderer/assets/images/apps/baidu-ai-search.webp?url'
|
||||||
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
|
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
|
||||||
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url'
|
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url'
|
||||||
|
import CiciAppLogo from '@renderer/assets/images/apps/cici.webp?url'
|
||||||
|
import CozeAppLogo from '@renderer/assets/images/apps/coze.webp?url'
|
||||||
import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
|
import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
|
||||||
|
import DifyAppLogo from '@renderer/assets/images/apps/dify.svg?url'
|
||||||
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
|
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
|
||||||
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url'
|
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url'
|
||||||
import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url'
|
import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url'
|
||||||
@@ -14,19 +19,27 @@ import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?
|
|||||||
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
|
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
|
||||||
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
|
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
|
||||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
|
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
|
||||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg?url'
|
import KimiAppLogo from '@renderer/assets/images/apps/kimi.webp?url'
|
||||||
|
import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url'
|
||||||
|
import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url'
|
||||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
|
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
|
||||||
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp?url'
|
import MonicaLogo from '@renderer/assets/images/apps/monica.webp?url'
|
||||||
|
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
|
||||||
|
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
|
||||||
|
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
|
||||||
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
||||||
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
||||||
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
|
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
|
||||||
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
|
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
|
||||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
|
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
|
||||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png?url'
|
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.webp?url'
|
||||||
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
|
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
|
||||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
||||||
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
||||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?url'
|
import WPSLingXiLogo from '@renderer/assets/images/apps/wpslingxi.webp?url'
|
||||||
|
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
|
||||||
|
import YouLogo from '@renderer/assets/images/apps/you.jpg?url'
|
||||||
|
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.webp?url'
|
||||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
|
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
|
||||||
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
||||||
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
||||||
@@ -109,6 +122,12 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
url: 'https://www.doubao.com/chat/',
|
url: 'https://www.doubao.com/chat/',
|
||||||
logo: DoubaoAppLogo
|
logo: DoubaoAppLogo
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'cici',
|
||||||
|
name: 'Cici',
|
||||||
|
url: 'https://www.cici.com/chat/',
|
||||||
|
logo: CiciAppLogo
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'minimax',
|
id: 'minimax',
|
||||||
name: '海螺',
|
name: '海螺',
|
||||||
@@ -133,6 +152,16 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
logo: BaiduAiAppLogo,
|
logo: BaiduAiAppLogo,
|
||||||
url: 'https://yiyan.baidu.com/'
|
url: 'https://yiyan.baidu.com/'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'baidu-ai-search',
|
||||||
|
name: '百度AI搜索',
|
||||||
|
logo: BaiduAiSearchLogo,
|
||||||
|
url: 'https://chat.baidu.com/',
|
||||||
|
bodered: true,
|
||||||
|
style: {
|
||||||
|
padding: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tencent-yuanbao',
|
id: 'tencent-yuanbao',
|
||||||
name: '腾讯元宝',
|
name: '腾讯元宝',
|
||||||
@@ -167,7 +196,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'perplexity',
|
id: 'perplexity',
|
||||||
name: 'perplexity',
|
name: 'Perplexity',
|
||||||
logo: PerplexityAppLogo,
|
logo: PerplexityAppLogo,
|
||||||
url: 'https://www.perplexity.ai/'
|
url: 'https://www.perplexity.ai/'
|
||||||
},
|
},
|
||||||
@@ -184,13 +213,6 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
url: 'https://www.tiangong.cn/',
|
url: 'https://www.tiangong.cn/',
|
||||||
bodered: true
|
bodered: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'zhihu-zhiada',
|
|
||||||
name: '知乎直答',
|
|
||||||
logo: ZhihuAppLogo,
|
|
||||||
url: 'https://zhida.zhihu.com/',
|
|
||||||
bodered: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'hugging-chat',
|
id: 'hugging-chat',
|
||||||
name: 'HuggingChat',
|
name: 'HuggingChat',
|
||||||
@@ -220,6 +242,13 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'nm',
|
id: 'nm',
|
||||||
|
name: '纳米AI',
|
||||||
|
logo: NamiAiLogo,
|
||||||
|
url: 'https://bot.n.cn/',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nm-search',
|
||||||
name: '纳米AI搜索',
|
name: '纳米AI搜索',
|
||||||
logo: NamiAiSearchLogo,
|
logo: NamiAiSearchLogo,
|
||||||
url: 'https://www.n.cn/',
|
url: 'https://www.n.cn/',
|
||||||
@@ -230,7 +259,10 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
name: 'ThinkAny',
|
name: 'ThinkAny',
|
||||||
logo: ThinkAnyLogo,
|
logo: ThinkAnyLogo,
|
||||||
url: 'https://thinkany.ai/',
|
url: 'https://thinkany.ai/',
|
||||||
bodered: true
|
bodered: true,
|
||||||
|
style: {
|
||||||
|
padding: 5
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hika',
|
id: 'hika',
|
||||||
@@ -283,6 +315,84 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
name: 'AI Studio',
|
name: 'AI Studio',
|
||||||
logo: AIStudioLogo,
|
logo: AIStudioLogo,
|
||||||
url: 'https://aistudio.google.com/'
|
url: 'https://aistudio.google.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xiaoyi',
|
||||||
|
name: '小艺',
|
||||||
|
logo: XiaoYiAppLogo,
|
||||||
|
url: 'https://xiaoyi.huawei.com/chat/',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notebooklm',
|
||||||
|
name: 'NotebookLM',
|
||||||
|
logo: NotebookLMAppLogo,
|
||||||
|
url: 'https://notebooklm.google.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'coze',
|
||||||
|
name: 'Coze',
|
||||||
|
logo: CozeAppLogo,
|
||||||
|
url: 'https://www.coze.com/space',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dify',
|
||||||
|
name: 'Dify',
|
||||||
|
logo: DifyAppLogo,
|
||||||
|
url: 'https://cloud.dify.ai/apps',
|
||||||
|
bodered: true,
|
||||||
|
style: {
|
||||||
|
padding: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wpslingxi',
|
||||||
|
name: 'WPS灵犀',
|
||||||
|
logo: WPSLingXiLogo,
|
||||||
|
url: 'https://copilot.wps.cn/',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lechat',
|
||||||
|
name: 'LeChat',
|
||||||
|
logo: LeChatLogo,
|
||||||
|
url: 'https://chat.mistral.ai/chat',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'abacus',
|
||||||
|
name: 'Abacus',
|
||||||
|
logo: AbacusLogo,
|
||||||
|
url: 'https://apps.abacus.ai/chatllm',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lambdachat',
|
||||||
|
name: 'Lambda Chat',
|
||||||
|
logo: LambdaChatLogo,
|
||||||
|
url: 'https://lambda.chat/',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'monica',
|
||||||
|
name: 'Monica',
|
||||||
|
logo: MonicaLogo,
|
||||||
|
url: 'https://monica.im/home/',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'you',
|
||||||
|
name: 'You',
|
||||||
|
logo: YouLogo,
|
||||||
|
url: 'https://you.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'zhihu',
|
||||||
|
name: '知乎直答',
|
||||||
|
logo: ZhihuAppLogo,
|
||||||
|
url: 'https://zhida.zhihu.com/',
|
||||||
|
bodered: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||