Compare commits
328 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
934ab1a374 | ||
|
|
33ac0937df | ||
|
|
f1c8922752 | ||
|
|
03bdbdb412 | ||
|
|
cf9d4c5370 | ||
|
|
bfa6bfa196 | ||
|
|
af8144d45e | ||
|
|
29605fbcdb | ||
|
|
6e7e5cb1f1 | ||
|
|
6f5dccd595 | ||
|
|
0af35b9f10 | ||
|
|
8350ac037e | ||
|
|
74b80b474e | ||
|
|
be4bf5b510 | ||
|
|
fdb610736d | ||
|
|
82e9baf211 | ||
|
|
e34d4be6f2 | ||
|
|
e7f7f8509e | ||
|
|
fa1f00f4f5 | ||
|
|
cee373bb6f | ||
|
|
01acdeb777 | ||
|
|
a654ccc25e | ||
|
|
71a35ccd44 | ||
|
|
29826ff091 | ||
|
|
8566476d91 | ||
|
|
a173a87f29 | ||
|
|
cb068d71ca | ||
|
|
66210d1d2e | ||
|
|
aa427c9911 | ||
|
|
9ae9fdf392 | ||
|
|
0ddef31ed8 | ||
|
|
617af8b12a | ||
|
|
71876e6a70 | ||
|
|
4f250cdcb1 | ||
|
|
9268ab845e | ||
|
|
0337c6649b | ||
|
|
8781388760 | ||
|
|
2016ba7062 | ||
|
|
a03d619e2f | ||
|
|
76d1f0bb1e | ||
|
|
2bad5a1184 | ||
|
|
94ba3aee05 | ||
|
|
563758f69f | ||
|
|
56af85cc3e | ||
|
|
6a1a861ecc | ||
|
|
ceab574a22 | ||
|
|
98704fdb28 | ||
|
|
fd5cba5219 | ||
|
|
be5aaa2b66 | ||
|
|
7e8687decd | ||
|
|
4c96324ef7 | ||
|
|
dd3c81ec5f | ||
|
|
42f0b5f8fc | ||
|
|
11b2cd88b7 | ||
|
|
6bf98f6db3 | ||
|
|
10b4e3c634 | ||
|
|
a3f5223b4c | ||
|
|
2855575b36 | ||
|
|
1f0ba20523 | ||
|
|
2f53416e09 | ||
|
|
ddbf266a3f | ||
|
|
d815415f36 | ||
|
|
cdacc56fd7 | ||
|
|
455d909c74 | ||
|
|
52d84afed6 | ||
|
|
f06d1d4d9a | ||
|
|
805a65bbaa | ||
|
|
f217950b13 | ||
|
|
9ff65441ef | ||
|
|
2b20282a41 | ||
|
|
96ad2de896 | ||
|
|
e1ea875c21 | ||
|
|
500e91977c | ||
|
|
bd194ff955 | ||
|
|
828bd71f22 | ||
|
|
5991f692b2 | ||
|
|
200d78a140 | ||
|
|
9a502b5e47 | ||
|
|
97ef3772ea | ||
|
|
eb18be200e | ||
|
|
467e97ff4b | ||
|
|
27b802d3c2 | ||
|
|
37b0a175f7 | ||
|
|
b2b79f12a2 | ||
|
|
885c578582 | ||
|
|
e61e4b109a | ||
|
|
f3bafbeb52 | ||
|
|
e55c0cdcef | ||
|
|
e73bbf4d6a | ||
|
|
3859289218 | ||
|
|
591bb45a4e | ||
|
|
b31f518fca | ||
|
|
dfbdb989db | ||
|
|
f194ebbc20 | ||
|
|
ab0e7e1e07 | ||
|
|
d809f50c0e | ||
|
|
a48d24de26 | ||
|
|
0dacc20e74 | ||
|
|
08df6cb4f8 | ||
|
|
0676ac8942 | ||
|
|
c257e8f0fe | ||
|
|
521670f683 | ||
|
|
87216b5d91 | ||
|
|
e6122a3d36 | ||
|
|
e6e1502308 | ||
|
|
7f5be3a688 | ||
|
|
4dde49a9f0 | ||
|
|
ce830b692b | ||
|
|
563472f3a9 | ||
|
|
14acd45927 | ||
|
|
9e2c7a08df | ||
|
|
f10c8dc379 | ||
|
|
fdd815879a | ||
|
|
635f238576 | ||
|
|
615e337e3f | ||
|
|
acd5d4b192 | ||
|
|
9a41b697c6 | ||
|
|
5cb67e00a6 | ||
|
|
350f13e97c | ||
|
|
4d6cbf5073 | ||
|
|
8d7b10d21e | ||
|
|
6753a93c0d | ||
|
|
9ee763337d | ||
|
|
ace0cb7823 | ||
|
|
44e518ef03 | ||
|
|
e28b96b45e | ||
|
|
11427a980c | ||
|
|
cb95562e58 | ||
|
|
89bdab58f7 | ||
|
|
d42ee59335 | ||
|
|
88e7ab211d | ||
|
|
5347bdfa83 | ||
|
|
c8711c5804 | ||
|
|
24cf3bb043 | ||
|
|
0531ecf3cf | ||
|
|
0cbfd26883 | ||
|
|
ee398489de | ||
|
|
71d7c2c738 | ||
|
|
b98f7298a2 | ||
|
|
de4f2599be | ||
|
|
93b32e8e21 | ||
|
|
e353d0f8ee | ||
|
|
dfd42fe9a6 | ||
|
|
a2dc325896 | ||
|
|
b131d320ea | ||
|
|
b88f4a869e | ||
|
|
461458e5ec | ||
|
|
4c2014f1d6 | ||
|
|
647dd3e751 | ||
|
|
4225312d4a | ||
|
|
c2a4613e32 | ||
|
|
5d5c1eee74 | ||
|
|
c1b5e6b183 | ||
|
|
fd37ba18dc | ||
|
|
4a26f7ce78 | ||
|
|
8b38ebcac4 | ||
|
|
e8dac28787 | ||
|
|
3ccebb503f | ||
|
|
42327836de | ||
|
|
4d7a3bb8c3 | ||
|
|
1996e163c9 | ||
|
|
e43f7f87ab | ||
|
|
47a83fa67f | ||
|
|
5e954566c9 | ||
|
|
b8960ef02c | ||
|
|
1866b00265 | ||
|
|
be0799a4c6 | ||
|
|
d0f5547419 | ||
|
|
076011b02b | ||
|
|
ba5c70c45a | ||
|
|
ebe74ffd05 | ||
|
|
d0bea0491f | ||
|
|
514e1a4796 | ||
|
|
2ffedadee4 | ||
|
|
7b72783ae7 | ||
|
|
4485a00395 | ||
|
|
77c0952635 | ||
|
|
e1c7a25b87 | ||
|
|
b0c479190c | ||
|
|
c7c3d28893 | ||
|
|
994ee8d7df | ||
|
|
57f9550891 | ||
|
|
0c0d1560db | ||
|
|
145d7ee748 | ||
|
|
52af23b931 | ||
|
|
f7151bd066 | ||
|
|
744a1fedba | ||
|
|
978432d910 | ||
|
|
b6cb1e4d84 | ||
|
|
0096783f26 | ||
|
|
4fc53d7c19 | ||
|
|
34d99b711c | ||
|
|
5dd74a1018 | ||
|
|
e028d0600f | ||
|
|
64ee3f2108 | ||
|
|
30a082b979 | ||
|
|
5a0927393d | ||
|
|
16c68dcdcb | ||
|
|
b6500977b0 | ||
|
|
78cf33e8bc | ||
|
|
2f62f04adf | ||
|
|
84915b1ede | ||
|
|
248c7ea20e | ||
|
|
1031d40ddb | ||
|
|
3d44fc2208 | ||
|
|
22e3c0e270 | ||
|
|
5d81874166 | ||
|
|
f7ef895ce6 | ||
|
|
beb40f5baf | ||
|
|
07613e65f5 | ||
|
|
6185068353 | ||
|
|
61934cd65c | ||
|
|
41f65b66ba | ||
|
|
5edb53ef7d | ||
|
|
167988927b | ||
|
|
a39beb3841 | ||
|
|
8719d5c330 | ||
|
|
a7427d6cb6 | ||
|
|
8759a50727 | ||
|
|
7ffa42caa0 | ||
|
|
b0a3d705ff | ||
|
|
de41199f7e | ||
|
|
cbd9f60cfc | ||
|
|
8a0e2890dd | ||
|
|
a8f3e2be6b | ||
|
|
297539bab7 | ||
|
|
911c2d0202 | ||
|
|
2969a05f10 | ||
|
|
5d90489a04 | ||
|
|
18fa1c92a4 | ||
|
|
937e62bf9d | ||
|
|
6291a463d8 | ||
|
|
681c93f5eb | ||
|
|
23687f119d | ||
|
|
0bcdffc159 | ||
|
|
b04b0cc8a6 | ||
|
|
c9a964d8f8 | ||
|
|
86fc4676ba | ||
|
|
527afa1357 | ||
|
|
384178c617 | ||
|
|
c53e35db76 | ||
|
|
c36075f0b5 | ||
|
|
5c95373a37 | ||
|
|
29d6d607da | ||
|
|
e64375a74c | ||
|
|
4689bb53e9 | ||
|
|
e00c66e54a | ||
|
|
62b0908dfa | ||
|
|
cb0b9de1e9 | ||
|
|
d8d4afbc0d | ||
|
|
c50ff4585a | ||
|
|
a5ee8548f3 | ||
|
|
15b286a095 | ||
|
|
d47d4a158d | ||
|
|
cd85dcddf8 | ||
|
|
925a9fb8ec | ||
|
|
17c3437e02 | ||
|
|
69293846fc | ||
|
|
20a7fbfc48 | ||
|
|
64d4b8450a | ||
|
|
f080fc5048 | ||
|
|
50f08124d7 | ||
|
|
b91081ef99 | ||
|
|
d869ec9a9b | ||
|
|
70c4354d6c | ||
|
|
527c4e77dc | ||
|
|
2483ce3bb4 | ||
|
|
db3f8b8bee | ||
|
|
45bf3d4e86 | ||
|
|
59b39dc41a | ||
|
|
a267a8d4c3 | ||
|
|
5b123f2c33 | ||
|
|
fe34fb3c25 | ||
|
|
e6359d2048 | ||
|
|
aa3b2d6290 | ||
|
|
c0e51c3992 | ||
|
|
8c80cc00b3 | ||
|
|
f961accd86 | ||
|
|
7de91d236d | ||
|
|
2fdf0acec6 | ||
|
|
40e76f3e53 | ||
|
|
d7b8721848 | ||
|
|
b91b0dd8e4 | ||
|
|
bb9b053924 | ||
|
|
5743046200 | ||
|
|
a507776c1e | ||
|
|
e74c828379 | ||
|
|
4b264c6a6b | ||
|
|
d21a4dce92 | ||
|
|
74df29604b | ||
|
|
8807783aa6 | ||
|
|
f81b38a362 | ||
|
|
d0280186bc | ||
|
|
9d96b826e2 | ||
|
|
ec20750e64 | ||
|
|
51f4653cde | ||
|
|
3625eefec4 | ||
|
|
1e1414d659 | ||
|
|
b7162663f2 | ||
|
|
1dd1bb5804 | ||
|
|
4dd6c46035 | ||
|
|
4036c36753 | ||
|
|
764aadd234 | ||
|
|
3d801f1552 | ||
|
|
bd865f0270 | ||
|
|
93505a4bc6 | ||
|
|
c43be11d20 | ||
|
|
8535edbdd1 | ||
|
|
731fb7860b | ||
|
|
4a32976483 | ||
|
|
dedabe320e | ||
|
|
235b481645 | ||
|
|
58c5ace678 | ||
|
|
973d24271b | ||
|
|
f434fe1231 | ||
|
|
a0c147ae3f | ||
|
|
8d7cde1231 | ||
|
|
87c04408de | ||
|
|
2592448c74 | ||
|
|
6f054874e8 | ||
|
|
40d687104e | ||
|
|
ac3cfe2878 | ||
|
|
e9a7735fce | ||
|
|
c1a8198575 | ||
|
|
8b45548b79 | ||
|
|
3f3b930819 | ||
|
|
a5d6e2c5c5 | ||
|
|
2993ab8dc1 |
@@ -6,4 +6,4 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ['unused-imports'],
|
plugins: ['unused-imports', 'simple-import-sort'],
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
@@ -14,12 +14,8 @@ module.exports = {
|
|||||||
'unused-imports/no-unused-imports': 'error',
|
'unused-imports/no-unused-imports': 'error',
|
||||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
'sort-imports': [
|
'simple-import-sort/imports': 'error',
|
||||||
'error',
|
'simple-import-sort/exports': 'error',
|
||||||
{
|
'react/no-is-mounted': 'off'
|
||||||
ignoreCase: true,
|
|
||||||
ignoreDeclarationSort: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, windows-latest]
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
draft: false
|
draft: true
|
||||||
files: |
|
files: |
|
||||||
dist/*.exe
|
dist/*.exe
|
||||||
dist/*.zip
|
dist/*.zip
|
||||||
|
|||||||
6
.gitignore
vendored
@@ -34,6 +34,9 @@ npm/*/*
|
|||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
@@ -42,3 +45,6 @@ out
|
|||||||
# ENV
|
# ENV
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
# Local
|
||||||
|
local
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
@@ -29,5 +29,6 @@
|
|||||||
},
|
},
|
||||||
"[markdown]": {
|
"[markdown]": {
|
||||||
"files.trimTrailingWhitespace": false
|
"files.trimTrailingWhitespace": false
|
||||||
}
|
},
|
||||||
|
"i18n-ally.localesPaths": ["src/renderer/src/i18n"]
|
||||||
}
|
}
|
||||||
|
|||||||
53
.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
diff --git a/lib/check-signature.js b/lib/check-signature.js
|
||||||
|
index 324568af71bcc4372c9f959131ecd24122848c86..677348e0a138ff608b2ac41f592d813b15ee4956 100644
|
||||||
|
--- a/lib/check-signature.js
|
||||||
|
+++ b/lib/check-signature.js
|
||||||
|
@@ -41,16 +41,12 @@ const spawn_1 = require("./spawn");
|
||||||
|
const debug_1 = __importDefault(require("debug"));
|
||||||
|
const d = (0, debug_1.default)('electron-notarize');
|
||||||
|
const codesignDisplay = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
|
- const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', path.basename(opts.appPath)], {
|
||||||
|
- cwd: path.dirname(opts.appPath),
|
||||||
|
- });
|
||||||
|
+ const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', opts.appPath]);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
const codesign = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
|
d('attempting to check codesign of app:', opts.appPath);
|
||||||
|
- const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', path.basename(opts.appPath)], {
|
||||||
|
- cwd: path.dirname(opts.appPath),
|
||||||
|
- });
|
||||||
|
+ const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', opts.appPath]);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
function checkSignatures(opts) {
|
||||||
|
diff --git a/lib/notarytool.js b/lib/notarytool.js
|
||||||
|
index 1ab090efb2101fc8bee5553445e0349c54474421..a5ddfd922197449fc56078e4a7e9a2ee5d8d207d 100644
|
||||||
|
--- a/lib/notarytool.js
|
||||||
|
+++ b/lib/notarytool.js
|
||||||
|
@@ -92,9 +92,7 @@ function notarizeAndWaitForNotaryTool(opts) {
|
||||||
|
else {
|
||||||
|
filePath = path.resolve(dir, `${path.parse(opts.appPath).name}.zip`);
|
||||||
|
d('zipping application to:', filePath);
|
||||||
|
- const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), filePath], {
|
||||||
|
- cwd: path.dirname(opts.appPath),
|
||||||
|
- });
|
||||||
|
+ const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', opts.appPath, filePath]);
|
||||||
|
if (zipResult.code !== 0) {
|
||||||
|
throw new Error(`Failed to zip application, exited with code: ${zipResult.code}\n\n${zipResult.output}`);
|
||||||
|
}
|
||||||
|
diff --git a/lib/staple.js b/lib/staple.js
|
||||||
|
index 47dbd85b2fc279d999b57f47fb8171e1cc674436..f8829e6ac54fcd630a730d12d75acc1591b953b6 100644
|
||||||
|
--- a/lib/staple.js
|
||||||
|
+++ b/lib/staple.js
|
||||||
|
@@ -43,9 +43,7 @@ const d = (0, debug_1.default)('electron-notarize:staple');
|
||||||
|
function stapleApp(opts) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
d('attempting to staple app:', opts.appPath);
|
||||||
|
- const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', path.basename(opts.appPath)], {
|
||||||
|
- cwd: path.dirname(opts.appPath),
|
||||||
|
- });
|
||||||
|
+ const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', opts.appPath]);
|
||||||
|
if (result.code !== 0) {
|
||||||
|
throw new Error(`Failed to staple your application with code: ${result.code}\n\n${result.output}`);
|
||||||
|
}
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
nodeLinker: node-modules
|
enableImmutableInstalls: false
|
||||||
|
|
||||||
httpTimeout: 300000
|
httpTimeout: 300000
|
||||||
|
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|||||||
114
LICENSE
@@ -1,21 +1,101 @@
|
|||||||
MIT License
|
### Cherry Studio 商业许可协议
|
||||||
|
|
||||||
Copyright (c) 2024 亢奋猫
|
---
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
#### 中文版
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
**Cherry Studio 商业许可协议**
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
本协议(以下简称“协议”)由以下双方签订:
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
- 许可方:王谦(kangfenmao@qq.com)
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
- 被许可方:[被许可方名称]
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
**1. 定义**
|
||||||
SOFTWARE.
|
|
||||||
|
- “软件”指 Cherry Studio 软件,网址为 https://cherry-ai.com。
|
||||||
|
- “商业用途”指任何以盈利为目的的使用。
|
||||||
|
|
||||||
|
**2. 许可**
|
||||||
|
|
||||||
|
- 未经许可方明确书面许可,被许可方不得将软件用于商业用途。
|
||||||
|
- 未经许可方事先书面同意,被许可方不得将软件全部或部分用于商业用途分发。
|
||||||
|
- 未经许可方明确授权,被许可方不得再许可、租赁、销售、出租或以其他方式将软件转让给任何第三方用于商业用途。
|
||||||
|
|
||||||
|
**3. 责任限制**
|
||||||
|
|
||||||
|
开发者不对因使用本软件而产生的任何直接或间接损失承担责任。用户应自行承担使用本软件的风险。
|
||||||
|
|
||||||
|
**4. 许可协议生效日期**
|
||||||
|
|
||||||
|
本许可协议自用户首次下载或使用本软件之日起生效。
|
||||||
|
|
||||||
|
**5. 许可终止**
|
||||||
|
|
||||||
|
如发现用户违反上述条款,开发者有权随时终止本许可,并要求用户停止使用本软件及删除所有相关副本。
|
||||||
|
|
||||||
|
**6. 其他**
|
||||||
|
|
||||||
|
本协议的解释、效力及争议的解决,均适用中华人民共和国法律。
|
||||||
|
|
||||||
|
**7. 联系信息**
|
||||||
|
|
||||||
|
- 许可方联系方式:
|
||||||
|
- 手机号:18539907620
|
||||||
|
- 邮箱:kangfenmao@qq.com
|
||||||
|
|
||||||
|
**许可方(签字):**
|
||||||
|
**日期:**
|
||||||
|
|
||||||
|
**被许可方(签字):**
|
||||||
|
**日期:**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### English Version
|
||||||
|
|
||||||
|
**Cherry Studio Commercial License Agreement**
|
||||||
|
|
||||||
|
This Agreement ("Agreement") is entered into by and between:
|
||||||
|
|
||||||
|
- Licensor: Wang Qian (kangfenmao)
|
||||||
|
- Licensee: [Licensee Name]
|
||||||
|
|
||||||
|
**1. Definitions**
|
||||||
|
|
||||||
|
- "Software" refers to the Cherry Studio software, available at https://cherry-ai.com.
|
||||||
|
- "Commercial Use" refers to any use for profit.
|
||||||
|
|
||||||
|
**2. License**
|
||||||
|
|
||||||
|
- The Licensee may not use the Software for Commercial Use without the Licensor's explicit written permission.
|
||||||
|
- The Licensee may not distribute the Software in whole or in part for Commercial Use without the Licensor's prior written consent.
|
||||||
|
- The Licensee may not sublicense, lease, sell, rent, or otherwise transfer the Software to any third party for Commercial Use without the Licensor's explicit authorization.
|
||||||
|
|
||||||
|
**3. Termination of License**
|
||||||
|
|
||||||
|
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
|
||||||
|
|
||||||
|
**4. Effective Date of License Agreement**
|
||||||
|
|
||||||
|
This license agreement becomes effective from the date the user first downloads or uses the software.
|
||||||
|
|
||||||
|
**5. Termination of License**
|
||||||
|
|
||||||
|
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
|
||||||
|
|
||||||
|
**6. Miscellaneous**
|
||||||
|
|
||||||
|
This Agreement shall be governed by and construed in accordance with the laws of the People's Republic of China.
|
||||||
|
|
||||||
|
**7. Contact Information**
|
||||||
|
|
||||||
|
- Licensor's Contact Details:
|
||||||
|
- Phone: 18539907620
|
||||||
|
- Email: kangfenmao@qq.com
|
||||||
|
|
||||||
|
**Licensor (Signature):**
|
||||||
|
**Date:**
|
||||||
|
|
||||||
|
**Licensee (Signature):**
|
||||||
|
**Date:**
|
||||||
|
|||||||
28
README.md
@@ -1,25 +1,27 @@
|
|||||||
# Cherry Studio
|
# 🍒 Cherry Studio
|
||||||
|
|
||||||
🍒 Cherry Studio is a desktop client that supports multiple Large Language Model (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.
|
||||||
|
|
||||||
# Screenshot
|
# 🌠 Screenshot
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
# Feature
|
# 🌟 Features
|
||||||
|
|
||||||
1. Supports multiple large language model service providers.
|
1. Support for Multiple LLM Providers.
|
||||||
2. Allows creation of multiple Assistants.
|
2. Allows creation of multiple Assistants.
|
||||||
3. Enables creation of multiple topics.
|
3. Enables creation of multiple topics.
|
||||||
4. Allows using multiple models to answer questions in the same conversation.
|
4. Allows using multiple models to answer questions in the same conversation.
|
||||||
5. Supports drag-and-drop sorting.
|
5. Supports drag-and-drop sorting.
|
||||||
6. Code highlighting.
|
6. Code highlighting.
|
||||||
|
7. Mermaid chart
|
||||||
|
|
||||||
|
# 🖥️ Develop
|
||||||
|
|
||||||
# Develop
|
|
||||||
## Recommended IDE Setup
|
## Recommended IDE Setup
|
||||||
|
|
||||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||||
@@ -50,3 +52,11 @@ $ yarn build:mac
|
|||||||
# For Linux
|
# For Linux
|
||||||
$ yarn build:linux
|
$ yarn build:linux
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# ⭐️ Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||||
|
|
||||||
|
# 📃 License
|
||||||
|
|
||||||
|
[LICENSE](./LICENSE)
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ productName: Cherry Studio
|
|||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!{.vscode,.yarn,.github}'
|
||||||
- '!src/*'
|
|
||||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
- '!src'
|
||||||
|
- '!local'
|
||||||
|
- '!scripts'
|
||||||
|
- '!resources'
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
@@ -40,8 +43,8 @@ dmg:
|
|||||||
linux:
|
linux:
|
||||||
target:
|
target:
|
||||||
- AppImage
|
- AppImage
|
||||||
- snap
|
# - snap
|
||||||
- deb
|
# - deb
|
||||||
maintainer: electronjs.org
|
maintainer: electronjs.org
|
||||||
category: Utility
|
category: Utility
|
||||||
appImage:
|
appImage:
|
||||||
@@ -56,6 +59,11 @@ electronDownload:
|
|||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
支持设置模型 Temperature 参数
|
本次更新:
|
||||||
支持设置上下文数量
|
增加了30多种文本文档格式选择
|
||||||
输入框增加 Token 消耗预估
|
支持粘贴图片和文件到聊天输入框
|
||||||
|
支持将对话移动到其他智能体了
|
||||||
|
近期更新:
|
||||||
|
支持 Vision 模型
|
||||||
|
新增文件功能
|
||||||
|
支持从特定消息创建新分支
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@types': resolve('src/renderer/src/types'),
|
||||||
|
'@main': resolve('src/main')
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
@@ -15,10 +21,6 @@ export default defineConfig({
|
|||||||
'@renderer': resolve('src/renderer/src')
|
'@renderer': resolve('src/renderer/src')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [react()],
|
plugins: [react()]
|
||||||
assetsInclude: ['**/*.md'],
|
|
||||||
server: {
|
|
||||||
host: '0.0.0.0'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
40
package.json
@@ -1,10 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "cherry-studio",
|
"name": "CherryStudio",
|
||||||
"version": "0.3.0",
|
"version": "0.7.2",
|
||||||
|
"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",
|
||||||
"author": "kangfenmao@qq.com",
|
"author": "kangfenmao@qq.com",
|
||||||
"homepage": "https://github.com/kangfenmao/cherry-studio",
|
"homepage": "https://github.com/kangfenmao/cherry-studio",
|
||||||
|
"workspaces": {
|
||||||
|
"packages": [
|
||||||
|
"local"
|
||||||
|
]
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||||
@@ -18,13 +24,15 @@
|
|||||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||||
"build:win": "dotenv npm run build && electron-builder --win --publish never",
|
"build:win": "dotenv npm run build && electron-builder --win --publish never",
|
||||||
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
|
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
|
||||||
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never"
|
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
|
||||||
|
"release": "node scripts/version.js",
|
||||||
|
"publish": "yarn release patch push"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@sentry/electron": "^5.2.0",
|
|
||||||
"electron-log": "^5.1.5",
|
"electron-log": "^5.1.5",
|
||||||
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"electron-window-state": "^5.0.3"
|
"electron-window-state": "^5.0.3"
|
||||||
},
|
},
|
||||||
@@ -33,7 +41,7 @@
|
|||||||
"@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",
|
||||||
"@fontsource/inter": "^5.0.18",
|
"@google/generative-ai": "^0.16.0",
|
||||||
"@hello-pangea/dnd": "^16.6.0",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||||
"@reduxjs/toolkit": "^2.2.5",
|
"@reduxjs/toolkit": "^2.2.5",
|
||||||
@@ -42,24 +50,29 @@
|
|||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"ahooks": "^3.8.0",
|
|
||||||
"antd": "^5.18.3",
|
"antd": "^5.18.3",
|
||||||
|
"axios": "^1.7.3",
|
||||||
"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-react-hooks": "^1.1.7",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"electron": "^28.2.0",
|
"electron": "^28.3.3",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.0.0",
|
||||||
"emittery": "^1.0.3",
|
"emittery": "^1.0.3",
|
||||||
|
"emoji-picker-element": "^1.22.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-react": "^7.34.3",
|
"eslint-plugin-react": "^7.34.3",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.0.0",
|
"eslint-plugin-unused-imports": "^4.0.0",
|
||||||
"gpt-tokens": "^1.3.6",
|
"gpt-tokens": "^1.3.10",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mime": "^4.0.4",
|
||||||
"openai": "^4.52.1",
|
"openai": "^4.52.1",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -69,11 +82,16 @@
|
|||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
"react-router-dom": "6",
|
"react-router-dom": "6",
|
||||||
|
"react-spinners": "^0.14.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
|
"rehype-katex": "^7.0.0",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
"sass": "^1.77.2",
|
"sass": "^1.77.2",
|
||||||
"styled-components": "^6.1.11",
|
"styled-components": "^6.1.11",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.6.2",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vite": "^5.0.12"
|
"vite": "^5.0.12"
|
||||||
},
|
},
|
||||||
@@ -82,7 +100,7 @@
|
|||||||
"react-dom": "^17.0.0 || ^18.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@electron/notarize": "2.3.2"
|
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.3.1"
|
"packageManager": "yarn@4.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
68
resources/graphrag.html
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="3d-graph"></div>
|
||||||
|
<script src="./js/bridge.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import { getQueryParam } from './js/utils.js'
|
||||||
|
|
||||||
|
const apiUrl = getQueryParam('apiUrl')
|
||||||
|
const modelId = getQueryParam('modelId')
|
||||||
|
const jsonUrl = `${apiUrl}/v1/global_graph/${modelId}`
|
||||||
|
|
||||||
|
const infoCard = document.createElement('div')
|
||||||
|
infoCard.style.position = 'fixed'
|
||||||
|
infoCard.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'
|
||||||
|
infoCard.style.padding = '8px'
|
||||||
|
infoCard.style.borderRadius = '4px'
|
||||||
|
infoCard.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'
|
||||||
|
infoCard.style.fontSize = '12px'
|
||||||
|
infoCard.style.maxWidth = '200px'
|
||||||
|
infoCard.style.display = 'none'
|
||||||
|
infoCard.style.zIndex = '1000'
|
||||||
|
document.body.appendChild(infoCard)
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (event) => {
|
||||||
|
infoCard.style.left = `${event.clientX + 10}px`
|
||||||
|
infoCard.style.top = `${event.clientY + 10}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
const elem = document.getElementById('3d-graph')
|
||||||
|
const Graph = ForceGraph3D()(elem)
|
||||||
|
.jsonUrl(jsonUrl)
|
||||||
|
.nodeAutoColorBy((node) => node.properties.type || 'default')
|
||||||
|
.nodeVal((node) => node.properties.degree)
|
||||||
|
.linkWidth((link) => link.properties.weight)
|
||||||
|
.onNodeHover((node) => {
|
||||||
|
if (node) {
|
||||||
|
infoCard.innerHTML = `
|
||||||
|
<div style="font-weight: bold; margin-bottom: 4px; color: #333;">
|
||||||
|
${node.properties.title}
|
||||||
|
</div>
|
||||||
|
<div style="color: #666;">
|
||||||
|
${node.properties.description}
|
||||||
|
</div>`
|
||||||
|
infoCard.style.display = 'block'
|
||||||
|
} else {
|
||||||
|
infoCard.style.display = 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onNodeClick((node) => {
|
||||||
|
const url = `${apiUrl}/v1/references/${modelId}/entities/${node.properties.human_readable_id}`
|
||||||
|
window.api.minApp({
|
||||||
|
url,
|
||||||
|
windowOptions: {
|
||||||
|
title: node.properties.title,
|
||||||
|
width: 500,
|
||||||
|
height: 800
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
Before Width: | Height: | Size: 197 KiB |
36
resources/js/bridge.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
;(() => {
|
||||||
|
let messageId = 0
|
||||||
|
const pendingCalls = new Map()
|
||||||
|
|
||||||
|
function api(method, ...args) {
|
||||||
|
const id = messageId++
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pendingCalls.set(id, { resolve, reject })
|
||||||
|
window.parent.postMessage({ id, type: 'api-call', method, args }, '*')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.data.type === 'api-response') {
|
||||||
|
const { id, result, error } = event.data
|
||||||
|
const pendingCall = pendingCalls.get(id)
|
||||||
|
if (pendingCall) {
|
||||||
|
if (error) {
|
||||||
|
pendingCall.reject(new Error(error))
|
||||||
|
} else {
|
||||||
|
pendingCall.resolve(result)
|
||||||
|
}
|
||||||
|
pendingCalls.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.api = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (target, prop) => {
|
||||||
|
return (...args) => api(prop, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})()
|
||||||
5
resources/js/utils.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export function getQueryParam(paramName) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const params = new URLSearchParams(url.search)
|
||||||
|
return params.get(paramName)
|
||||||
|
}
|
||||||
40
scripts/version.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const { execSync } = require('child_process')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
// 执行命令并返回输出
|
||||||
|
function exec(command) {
|
||||||
|
return execSync(command, { encoding: 'utf8' }).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取命令行参数
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const versionType = args[0] || 'patch'
|
||||||
|
const shouldPush = args.includes('push')
|
||||||
|
|
||||||
|
// 验证版本类型
|
||||||
|
if (!['patch', 'minor', 'major'].includes(versionType)) {
|
||||||
|
console.error('Invalid version type. Use patch, minor, or major.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新版本
|
||||||
|
exec(`yarn version ${versionType} --immediate`)
|
||||||
|
|
||||||
|
// 读取更新后的 package.json 获取新版本号
|
||||||
|
const updatedPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||||
|
const newVersion = updatedPackageJson.version
|
||||||
|
|
||||||
|
// Git 操作
|
||||||
|
exec('git add .')
|
||||||
|
exec(`git commit -m "chore(version): ${newVersion}"`)
|
||||||
|
exec(`git tag -a v${newVersion} -m "Version ${newVersion}"`)
|
||||||
|
|
||||||
|
console.log(`Version bumped to ${newVersion}`)
|
||||||
|
|
||||||
|
if (shouldPush) {
|
||||||
|
console.log('Pushing to remote...')
|
||||||
|
exec('git push && git push --tags')
|
||||||
|
console.log('Pushed to remote.')
|
||||||
|
} else {
|
||||||
|
console.log('Changes are committed locally. Use "git push && git push --tags" to push to remote.')
|
||||||
|
}
|
||||||
33
src/main/config.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
import { app } from 'electron'
|
||||||
|
import Store from 'electron-store'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
isDev && app.setPath('userData', app.getPath('userData') + 'Dev')
|
||||||
|
|
||||||
|
const getDataPath = () => {
|
||||||
|
const dataPath = path.join(app.getPath('userData'), 'Data')
|
||||||
|
if (!fs.existsSync(dataPath)) {
|
||||||
|
fs.mkdirSync(dataPath, { recursive: true })
|
||||||
|
}
|
||||||
|
return dataPath
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DATA_PATH = getDataPath()
|
||||||
|
|
||||||
|
export const appConfig = new Store()
|
||||||
|
|
||||||
|
export const titleBarOverlayDark = {
|
||||||
|
height: 41,
|
||||||
|
color: '#00000000',
|
||||||
|
symbolColor: '#ffffff'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const titleBarOverlayLight = {
|
||||||
|
height: 41,
|
||||||
|
color: '#00000000',
|
||||||
|
symbolColor: '#000000'
|
||||||
|
}
|
||||||
9
src/main/env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
VITE_MAIN_BUNDLE_ID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -1,78 +1,19 @@
|
|||||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
import * as Sentry from '@sentry/electron/main'
|
import { app, BrowserWindow } from 'electron'
|
||||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
|
||||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
|
||||||
import { join } from 'path'
|
|
||||||
import icon from '../../resources/icon.png?asset'
|
|
||||||
import AppUpdater from './updater'
|
|
||||||
|
|
||||||
function createWindow() {
|
import { registerIpc } from './ipc'
|
||||||
// Load the previous state with fallback to defaults
|
import { updateUserDataPath } from './utils/upgrade'
|
||||||
const mainWindowState = windowStateKeeper({
|
import { createMainWindow } from './window'
|
||||||
defaultWidth: 1080,
|
|
||||||
defaultHeight: 670
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create the browser window.
|
|
||||||
const mainWindow = new BrowserWindow({
|
|
||||||
x: mainWindowState.x,
|
|
||||||
y: mainWindowState.y,
|
|
||||||
width: mainWindowState.width,
|
|
||||||
height: mainWindowState.height,
|
|
||||||
minWidth: 1080,
|
|
||||||
minHeight: 500,
|
|
||||||
show: true,
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
titleBarStyle: 'hiddenInset',
|
|
||||||
trafficLightPosition: { x: 8, y: 12 },
|
|
||||||
...(process.platform === 'linux' ? { icon } : {}),
|
|
||||||
webPreferences: {
|
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
|
||||||
sandbox: false,
|
|
||||||
devTools: !app.isPackaged,
|
|
||||||
webSecurity: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindowState.manage(mainWindow)
|
|
||||||
|
|
||||||
mainWindow.webContents.on('context-menu', () => {
|
|
||||||
const menu = new Menu()
|
|
||||||
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
|
|
||||||
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
|
|
||||||
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
|
|
||||||
menu.append(new MenuItem({ type: 'separator' }))
|
|
||||||
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
|
|
||||||
menu.popup()
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
|
||||||
mainWindow.show()
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
|
||||||
shell.openExternal(details.url)
|
|
||||||
return { action: 'deny' }
|
|
||||||
})
|
|
||||||
|
|
||||||
// HMR for renderer base on electron-vite cli.
|
|
||||||
// Load the remote URL for development or the local html file for production.
|
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
|
||||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
|
||||||
} else {
|
|
||||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return mainWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(async () => {
|
||||||
|
await updateUserDataPath()
|
||||||
|
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('com.kangfenmao.CherryStudio')
|
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
// Default open or close DevTools by F12 in development
|
||||||
// and ignore CommandOrControl + R in production.
|
// and ignore CommandOrControl + R in production.
|
||||||
@@ -84,33 +25,18 @@ app.whenReady().then(() => {
|
|||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
// On macOS it's common to re-create a window in the app when the
|
||||||
// dock icon is clicked and there are no other windows open.
|
// dock icon is clicked and there are no other windows open.
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainWindow = createWindow()
|
const mainWindow = createMainWindow()
|
||||||
|
|
||||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
registerIpc(mainWindow, app)
|
||||||
|
|
||||||
// IPC
|
if (process.env.NODE_ENV === 'development') {
|
||||||
ipcMain.handle('get-app-info', () => ({
|
installExtension(REDUX_DEVTOOLS)
|
||||||
version: app.getVersion(),
|
.then((name) => console.log(`Added Extension: ${name}`))
|
||||||
isPackaged: app.isPackaged
|
.catch((err) => console.log('An error occurred: ', err))
|
||||||
}))
|
}
|
||||||
|
|
||||||
ipcMain.handle('open-website', (_, url: string) => {
|
|
||||||
shell.openExternal(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
|
||||||
ipcMain.handle('check-for-update', async () => {
|
|
||||||
autoUpdater.logger?.info('触发检查更新')
|
|
||||||
return {
|
|
||||||
currentVersion: autoUpdater.currentVersion,
|
|
||||||
update: await autoUpdater.checkForUpdates()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
installExtension(REDUX_DEVTOOLS)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
@@ -124,6 +50,3 @@ app.on('window-all-closed', () => {
|
|||||||
|
|
||||||
// In this file you can include the rest of your app"s specific main process
|
// In this file you can include the rest of your app"s specific main process
|
||||||
// code. You can also put them in separate files and require them here.
|
// code. You can also put them in separate files and require them here.
|
||||||
Sentry.init({
|
|
||||||
dsn: 'https://f0e972deff79c2df3e887e232d8a46a3@o4507610668007424.ingest.us.sentry.io/4507610670563328'
|
|
||||||
})
|
|
||||||
|
|||||||
75
src/main/ipc.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { FileType } from '@types'
|
||||||
|
import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron'
|
||||||
|
|
||||||
|
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||||
|
import AppUpdater from './services/AppUpdater'
|
||||||
|
import FileManager from './services/FileManager'
|
||||||
|
import { openFile, saveFile } from './utils/file'
|
||||||
|
import { compress, decompress } from './utils/zip'
|
||||||
|
import { createMinappWindow } from './window'
|
||||||
|
|
||||||
|
const fileManager = new FileManager()
|
||||||
|
|
||||||
|
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||||
|
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||||
|
|
||||||
|
// IPC
|
||||||
|
ipcMain.handle('get-app-info', () => ({
|
||||||
|
version: app.getVersion(),
|
||||||
|
isPackaged: app.isPackaged,
|
||||||
|
appPath: app.getAppPath()
|
||||||
|
}))
|
||||||
|
|
||||||
|
ipcMain.handle('open-website', (_, url: string) => {
|
||||||
|
shell.openExternal(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('set-proxy', (_, proxy: string) => {
|
||||||
|
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('save-file', saveFile)
|
||||||
|
ipcMain.handle('open-file', openFile)
|
||||||
|
ipcMain.handle('reload', () => mainWindow.reload())
|
||||||
|
|
||||||
|
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
||||||
|
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
||||||
|
|
||||||
|
ipcMain.handle('file:base64Image', async (_, id) => await fileManager.base64Image(id))
|
||||||
|
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
|
||||||
|
ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file))
|
||||||
|
ipcMain.handle('file:clear', async () => await fileManager.clear())
|
||||||
|
ipcMain.handle('file:read', async (_, id: string) => await fileManager.readFile(id))
|
||||||
|
ipcMain.handle('file:delete', async (_, id: string) => await fileManager.deleteFile(id))
|
||||||
|
ipcMain.handle('file:get', async (_, filePath: string) => await fileManager.getFile(filePath))
|
||||||
|
ipcMain.handle('file:create', async (_, fileName: string) => await fileManager.createTempFile(fileName))
|
||||||
|
ipcMain.handle(
|
||||||
|
'file:write',
|
||||||
|
async (_, filePath: string, data: Uint8Array | string) => await fileManager.writeFile(filePath, data)
|
||||||
|
)
|
||||||
|
|
||||||
|
ipcMain.handle('minapp', (_, args) => {
|
||||||
|
createMinappWindow({
|
||||||
|
url: args.url,
|
||||||
|
parent: mainWindow,
|
||||||
|
windowOptions: {
|
||||||
|
...mainWindow.getBounds(),
|
||||||
|
...args.windowOptions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
||||||
|
appConfig.set('theme', theme)
|
||||||
|
mainWindow?.setTitleBarOverlay &&
|
||||||
|
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||||
|
ipcMain.handle('check-for-update', async () => {
|
||||||
|
return {
|
||||||
|
currentVersion: autoUpdater.currentVersion,
|
||||||
|
update: await autoUpdater.checkForUpdates()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
|
||||||
import logger from 'electron-log'
|
|
||||||
import { BrowserWindow, dialog } from 'electron'
|
import { BrowserWindow, dialog } from 'electron'
|
||||||
|
import logger from 'electron-log'
|
||||||
|
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
||||||
|
|
||||||
export default class AppUpdater {
|
export default class AppUpdater {
|
||||||
autoUpdater: _AppUpdater = autoUpdater
|
autoUpdater: _AppUpdater = autoUpdater
|
||||||
@@ -17,11 +17,6 @@ export default class AppUpdater {
|
|||||||
mainWindow.webContents.send('update-error', error)
|
mainWindow.webContents.send('update-error', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检测是否需要更新
|
|
||||||
autoUpdater.on('checking-for-update', () => {
|
|
||||||
logger.info('正在检查更新……')
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
||||||
mainWindow.webContents.send('update-available', releaseInfo)
|
mainWindow.webContents.send('update-available', releaseInfo)
|
||||||
@@ -59,7 +54,6 @@ export default class AppUpdater {
|
|||||||
|
|
||||||
// 检测到不需要更新时
|
// 检测到不需要更新时
|
||||||
autoUpdater.on('update-not-available', () => {
|
autoUpdater.on('update-not-available', () => {
|
||||||
logger.info('现在使用的就是最新版本,不用更新')
|
|
||||||
mainWindow.webContents.send('update-not-available')
|
mainWindow.webContents.send('update-not-available')
|
||||||
})
|
})
|
||||||
|
|
||||||
198
src/main/services/FileManager.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { getFileType } from '@main/utils/file'
|
||||||
|
import { FileType } from '@types'
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
import { app, dialog, OpenDialogOptions } from 'electron'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
class FileManager {
|
||||||
|
private storageDir: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
|
||||||
|
this.initStorageDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
private initStorageDir(): void {
|
||||||
|
if (!fs.existsSync(this.storageDir)) {
|
||||||
|
fs.mkdirSync(this.storageDir, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getFileHash(filePath: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = crypto.createHash('md5')
|
||||||
|
const stream = fs.createReadStream(filePath)
|
||||||
|
stream.on('data', (data) => hash.update(data))
|
||||||
|
stream.on('end', () => resolve(hash.digest('hex')))
|
||||||
|
stream.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async findDuplicateFile(filePath: string): Promise<FileType | null> {
|
||||||
|
const stats = fs.statSync(filePath)
|
||||||
|
const fileSize = stats.size
|
||||||
|
|
||||||
|
const files = await fs.promises.readdir(this.storageDir)
|
||||||
|
for (const file of files) {
|
||||||
|
const storedFilePath = path.join(this.storageDir, file)
|
||||||
|
const storedStats = fs.statSync(storedFilePath)
|
||||||
|
|
||||||
|
if (storedStats.size === fileSize) {
|
||||||
|
const [originalHash, storedHash] = await Promise.all([
|
||||||
|
this.getFileHash(filePath),
|
||||||
|
this.getFileHash(storedFilePath)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (originalHash === storedHash) {
|
||||||
|
const ext = path.extname(file)
|
||||||
|
const id = path.basename(file, ext)
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
origin_name: file,
|
||||||
|
name: file + ext,
|
||||||
|
path: storedFilePath,
|
||||||
|
created_at: storedStats.birthtime,
|
||||||
|
size: storedStats.size,
|
||||||
|
ext,
|
||||||
|
type: getFileType(ext),
|
||||||
|
count: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectFile(options?: OpenDialogOptions): Promise<FileType[] | null> {
|
||||||
|
const defaultOptions: OpenDialogOptions = {
|
||||||
|
properties: ['openFile']
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogOptions = { ...defaultOptions, ...options }
|
||||||
|
|
||||||
|
const result = await dialog.showOpenDialog(dialogOptions)
|
||||||
|
|
||||||
|
if (result.canceled || result.filePaths.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileMetadataPromises = result.filePaths.map(async (filePath) => {
|
||||||
|
const stats = fs.statSync(filePath)
|
||||||
|
const ext = path.extname(filePath)
|
||||||
|
const fileType = getFileType(ext)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
origin_name: path.basename(filePath),
|
||||||
|
name: path.basename(filePath),
|
||||||
|
path: filePath,
|
||||||
|
created_at: stats.birthtime,
|
||||||
|
size: stats.size,
|
||||||
|
ext: ext,
|
||||||
|
type: fileType,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.all(fileMetadataPromises)
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile(file: FileType): Promise<FileType> {
|
||||||
|
const duplicateFile = await this.findDuplicateFile(file.path)
|
||||||
|
|
||||||
|
if (duplicateFile) {
|
||||||
|
return duplicateFile
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = uuidv4()
|
||||||
|
const origin_name = path.basename(file.path)
|
||||||
|
const ext = path.extname(origin_name)
|
||||||
|
const destPath = path.join(this.storageDir, uuid + ext)
|
||||||
|
|
||||||
|
await fs.promises.copyFile(file.path, destPath)
|
||||||
|
const stats = await fs.promises.stat(destPath)
|
||||||
|
const fileType = getFileType(ext)
|
||||||
|
|
||||||
|
const fileMetadata: FileType = {
|
||||||
|
id: uuid,
|
||||||
|
origin_name,
|
||||||
|
name: uuid + ext,
|
||||||
|
path: destPath,
|
||||||
|
created_at: stats.birthtime,
|
||||||
|
size: stats.size,
|
||||||
|
ext: ext,
|
||||||
|
type: fileType,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFile(filePath: string): Promise<FileType | null> {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(filePath)
|
||||||
|
const ext = path.extname(filePath)
|
||||||
|
const fileType = getFileType(ext)
|
||||||
|
|
||||||
|
const fileInfo: FileType = {
|
||||||
|
id: uuidv4(),
|
||||||
|
origin_name: path.basename(filePath),
|
||||||
|
name: path.basename(filePath),
|
||||||
|
path: filePath,
|
||||||
|
created_at: stats.birthtime,
|
||||||
|
size: stats.size,
|
||||||
|
ext: ext,
|
||||||
|
type: fileType,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(id: string): Promise<void> {
|
||||||
|
await fs.promises.unlink(path.join(this.storageDir, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(id: string): Promise<string> {
|
||||||
|
const filePath = path.join(this.storageDir, id)
|
||||||
|
return fs.readFileSync(filePath, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTempFile(fileName: string): Promise<string> {
|
||||||
|
const tempDir = path.join(app.getPath('temp'), 'CherryStudio')
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true })
|
||||||
|
}
|
||||||
|
const tempFilePath = path.join(tempDir, `temp_file_${uuidv4()}_${fileName}`)
|
||||||
|
return tempFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(filePath: string, data: Uint8Array | string): Promise<void> {
|
||||||
|
await fs.promises.writeFile(filePath, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async base64Image(id: string): Promise<{ mime: string; base64: string; data: string }> {
|
||||||
|
const filePath = path.join(this.storageDir, id)
|
||||||
|
const data = await fs.promises.readFile(filePath)
|
||||||
|
const base64 = data.toString('base64')
|
||||||
|
const mime = `image/${path.extname(filePath).slice(1)}`
|
||||||
|
return {
|
||||||
|
mime,
|
||||||
|
base64,
|
||||||
|
data: `data:${mime};base64,${base64}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
await fs.promises.rmdir(this.storageDir, { recursive: true })
|
||||||
|
await this.initStorageDir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileManager
|
||||||
24
src/main/utils/aes.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as crypto from 'crypto'
|
||||||
|
|
||||||
|
// 定义密钥和初始化向量(IV)
|
||||||
|
const secretKey = 'kDQvWz5slot3syfucoo53X6KKsEUJoeFikpiUWRJTLIo3zcUPpFvEa009kK13KCr'
|
||||||
|
const iv = Buffer.from('Cherry Studio', 'hex')
|
||||||
|
|
||||||
|
// 加密函数
|
||||||
|
export function encrypt(text: string): { iv: string; encryptedData: string } {
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(secretKey), iv)
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex')
|
||||||
|
encrypted += cipher.final('hex')
|
||||||
|
return {
|
||||||
|
iv: iv.toString('hex'),
|
||||||
|
encryptedData: encrypted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密函数
|
||||||
|
export function decrypt(encryptedData: string, iv: string): string {
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(secretKey), Buffer.from(iv, 'hex'))
|
||||||
|
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
|
||||||
|
decrypted += decipher.final('utf8')
|
||||||
|
return decrypted
|
||||||
|
}
|
||||||
158
src/main/utils/file.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||||
|
import logger from 'electron-log'
|
||||||
|
import { writeFileSync } from 'fs'
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
|
import { FileTypes } from '../../renderer/src/types'
|
||||||
|
|
||||||
|
export async function saveFile(
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
fileName: string,
|
||||||
|
content: string,
|
||||||
|
options?: SaveDialogOptions
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
|
||||||
|
title: '保存文件',
|
||||||
|
defaultPath: fileName,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePath) {
|
||||||
|
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openFile(
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
options: OpenDialogOptions
|
||||||
|
): Promise<{ fileName: string; content: Buffer } | null> {
|
||||||
|
try {
|
||||||
|
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
|
||||||
|
title: '打开文件',
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [{ name: '所有文件', extensions: ['*'] }],
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
const filePath = result.filePaths[0]
|
||||||
|
const fileName = filePath.split('/').pop() || ''
|
||||||
|
const content = await readFile(filePath)
|
||||||
|
return { fileName, content }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[IPC - Error]', 'An error occurred opening the file:', err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileType(ext: string): FileTypes {
|
||||||
|
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||||
|
const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||||
|
const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||||
|
const documentExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']
|
||||||
|
const textExts = [
|
||||||
|
'.txt', // 普通文本文件
|
||||||
|
'.md', // Markdown 文件
|
||||||
|
'.mdx', // Markdown 文件
|
||||||
|
'.html', // HTML 文件
|
||||||
|
'.htm', // HTML 文件的另一种扩展名
|
||||||
|
'.xml', // XML 文件
|
||||||
|
'.json', // JSON 文件
|
||||||
|
'.yaml', // YAML 文件
|
||||||
|
'.yml', // YAML 文件的另一种扩展名
|
||||||
|
'.csv', // 逗号分隔值文件
|
||||||
|
'.tsv', // 制表符分隔值文件
|
||||||
|
'.ini', // 配置文件
|
||||||
|
'.log', // 日志文件
|
||||||
|
'.rtf', // 富文本格式文件
|
||||||
|
'.tex', // LaTeX 文件
|
||||||
|
'.srt', // 字幕文件
|
||||||
|
'.xhtml', // XHTML 文件
|
||||||
|
'.nfo', // 信息文件(主要用于场景发布)
|
||||||
|
'.conf', // 配置文件
|
||||||
|
'.config', // 配置文件
|
||||||
|
'.env', // 环境变量文件
|
||||||
|
'.properties', // 配置属性文件
|
||||||
|
'.latex', // LaTeX 文档文件
|
||||||
|
'.rst', // reStructuredText 文件
|
||||||
|
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||||
|
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||||
|
'.ts', // TypeScript 文件
|
||||||
|
'.jsp', // JavaServer Pages 文件
|
||||||
|
'.aspx', // ASP.NET 文件
|
||||||
|
'.bat', // Windows 批处理文件
|
||||||
|
'.sh', // Unix/Linux Shell 脚本文件
|
||||||
|
'.py', // Python 脚本文件
|
||||||
|
'.rb', // Ruby 脚本文件
|
||||||
|
'.pl', // Perl 脚本文件
|
||||||
|
'.sql', // SQL 脚本文件
|
||||||
|
'.css', // Cascading Style Sheets 文件
|
||||||
|
'.less', // Less CSS 预处理器文件
|
||||||
|
'.scss', // Sass CSS 预处理器文件
|
||||||
|
'.sass', // Sass 文件
|
||||||
|
'.styl', // Stylus CSS 预处理器文件
|
||||||
|
'.coffee', // CoffeeScript 文件
|
||||||
|
'.ino', // Arduino 代码文件
|
||||||
|
'.ino', // Arduino 代码文件
|
||||||
|
'.asm', // Assembly 语言文件
|
||||||
|
'.go', // Go 语言文件
|
||||||
|
'.scala', // Scala 语言文件
|
||||||
|
'.swift', // Swift 语言文件
|
||||||
|
'.kt', // Kotlin 语言文件
|
||||||
|
'.rs', // Rust 语言文件
|
||||||
|
'.lua', // Lua 语言文件
|
||||||
|
'.groovy', // Groovy 语言文件
|
||||||
|
'.dart', // Dart 语言文件
|
||||||
|
'.hs', // Haskell 语言文件
|
||||||
|
'.clj', // Clojure 语言文件
|
||||||
|
'.cljs', // ClojureScript 语言文件
|
||||||
|
'.elm', // Elm 语言文件
|
||||||
|
'.erl', // Erlang 语言文件
|
||||||
|
'.ex', // Elixir 语言文件
|
||||||
|
'.exs', // Elixir 脚本文件
|
||||||
|
'.pug', // Pug (formerly Jade) 模板文件
|
||||||
|
'.haml', // Haml 模板文件
|
||||||
|
'.slim', // Slim 模板文件
|
||||||
|
'.tpl', // 模板文件(通用)
|
||||||
|
'.ejs', // Embedded JavaScript 模板文件
|
||||||
|
'.hbs', // Handlebars 模板文件
|
||||||
|
'.mustache', // Mustache 模板文件
|
||||||
|
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||||
|
'.twig', // Twig 模板文件
|
||||||
|
'.blade', // Blade 模板文件 (Laravel)
|
||||||
|
'.vue', // Vue.js 单文件组件
|
||||||
|
'.jsx', // React JSX 文件
|
||||||
|
'.tsx', // React TSX 文件
|
||||||
|
'.graphql', // GraphQL 查询语言文件
|
||||||
|
'.gql', // GraphQL 查询语言文件
|
||||||
|
'.proto', // Protocol Buffers 文件
|
||||||
|
'.thrift', // Thrift 文件
|
||||||
|
'.toml', // TOML 配置文件
|
||||||
|
'.edn', // Clojure 数据表示文件
|
||||||
|
'.cake', // CakePHP 配置文件
|
||||||
|
'.ctp', // CakePHP 视图文件
|
||||||
|
'.cfm', // ColdFusion 标记语言文件
|
||||||
|
'.cfc', // ColdFusion 组件文件
|
||||||
|
'.m', // Objective-C 源文件
|
||||||
|
'.mm', // Objective-C++ 源文件
|
||||||
|
'.gradle', // Gradle 构建文件
|
||||||
|
'.groovy', // Gradle 构建文件
|
||||||
|
'.gradle', // Gradle 构建文件
|
||||||
|
'.kts' // Kotlin Script 文件
|
||||||
|
]
|
||||||
|
|
||||||
|
ext = ext.toLowerCase()
|
||||||
|
if (imageExts.includes(ext)) return FileTypes.IMAGE
|
||||||
|
if (videoExts.includes(ext)) return FileTypes.VIDEO
|
||||||
|
if (audioExts.includes(ext)) return FileTypes.AUDIO
|
||||||
|
if (textExts.includes(ext)) return FileTypes.TEXT
|
||||||
|
if (documentExts.includes(ext)) return FileTypes.DOCUMENT
|
||||||
|
return FileTypes.OTHER
|
||||||
|
}
|
||||||
7
src/main/utils/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
export function getResourcePath() {
|
||||||
|
return path.join(app.getAppPath(), 'resources')
|
||||||
|
}
|
||||||
77
src/main/utils/upgrade.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { spawn } from 'child_process'
|
||||||
|
import { app, dialog } from 'electron'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export async function updateUserDataPath() {
|
||||||
|
const currentPath = app.getPath('userData')
|
||||||
|
const oldPath = currentPath.replace('CherryStudio', 'cherry-studio')
|
||||||
|
|
||||||
|
if (currentPath !== oldPath && fs.existsSync(oldPath)) {
|
||||||
|
Logger.log('Update userData path')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows 系统:创建 bat 文件
|
||||||
|
const batPath = await createWindowsBatFile(oldPath, currentPath)
|
||||||
|
await promptRestartAndExecute(batPath)
|
||||||
|
} else {
|
||||||
|
// 其他系统:直接更新
|
||||||
|
fs.rmSync(currentPath, { recursive: true, force: true })
|
||||||
|
fs.renameSync(oldPath, currentPath)
|
||||||
|
Logger.log(`Directory renamed: ${currentPath}`)
|
||||||
|
await promptRestart()
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('Error updating userData path:', error)
|
||||||
|
dialog.showErrorBox('错误', `更新用户数据目录时发生错误: ${error.message}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.log('userData path does not need to be updated')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWindowsBatFile(oldPath: string, currentPath: string): Promise<string> {
|
||||||
|
const batPath = path.join(app.getPath('temp'), 'rename_userdata.bat')
|
||||||
|
const appPath = app.getPath('exe')
|
||||||
|
const batContent = `
|
||||||
|
@echo off
|
||||||
|
timeout /t 2 /nobreak
|
||||||
|
rmdir /s /q "${currentPath}"
|
||||||
|
rename "${oldPath}" "${path.basename(currentPath)}"
|
||||||
|
start "" "${appPath}"
|
||||||
|
del "%~f0"
|
||||||
|
`
|
||||||
|
fs.writeFileSync(batPath, batContent)
|
||||||
|
return batPath
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptRestartAndExecute(batPath: string) {
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: '应用需要重启',
|
||||||
|
message: '用户数据目录将在重启后更新。请重启应用以应用更改。',
|
||||||
|
buttons: ['手动重启']
|
||||||
|
})
|
||||||
|
|
||||||
|
// 执行 bat 文件
|
||||||
|
spawn('cmd.exe', ['/c', batPath], {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore'
|
||||||
|
})
|
||||||
|
|
||||||
|
app.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptRestart() {
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: '应用需要重启',
|
||||||
|
message: '用户数据目录已更新。请重启应用以应用更改。',
|
||||||
|
buttons: ['重启']
|
||||||
|
})
|
||||||
|
|
||||||
|
app.relaunch()
|
||||||
|
app.exit(0)
|
||||||
|
}
|
||||||
39
src/main/utils/zip.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import util from 'node:util'
|
||||||
|
import zlib from 'node:zlib'
|
||||||
|
|
||||||
|
import logger from 'electron-log'
|
||||||
|
|
||||||
|
// 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本
|
||||||
|
const gzipPromise = util.promisify(zlib.gzip)
|
||||||
|
const gunzipPromise = util.promisify(zlib.gunzip)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩字符串
|
||||||
|
* @param {string} string - 要压缩的 JSON 字符串
|
||||||
|
* @returns {Promise<Buffer>} 压缩后的 Buffer
|
||||||
|
*/
|
||||||
|
export async function compress(str) {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.from(str, 'utf-8')
|
||||||
|
const compressedBuffer = await gzipPromise(buffer)
|
||||||
|
return compressedBuffer
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Compression failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压缩 Buffer 到 JSON 字符串
|
||||||
|
* @param {Buffer} compressedBuffer - 压缩的 Buffer
|
||||||
|
* @returns {Promise<string>} 解压缩后的 JSON 字符串
|
||||||
|
*/
|
||||||
|
export async function decompress(compressedBuffer) {
|
||||||
|
try {
|
||||||
|
const buffer = await gunzipPromise(compressedBuffer)
|
||||||
|
return buffer.toString('utf-8')
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Decompression failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/main/window.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { is } from '@electron-toolkit/utils'
|
||||||
|
import { BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||||
|
import windowStateKeeper from 'electron-window-state'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
import icon from '../../build/icon.png?asset'
|
||||||
|
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||||
|
|
||||||
|
export function createMainWindow() {
|
||||||
|
// Load the previous state with fallback to defaults
|
||||||
|
const mainWindowState = windowStateKeeper({
|
||||||
|
defaultWidth: 1080,
|
||||||
|
defaultHeight: 670
|
||||||
|
})
|
||||||
|
|
||||||
|
const theme = appConfig.get('theme') || 'light'
|
||||||
|
|
||||||
|
// Create the browser window.
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
x: mainWindowState.x,
|
||||||
|
y: mainWindowState.y,
|
||||||
|
width: mainWindowState.width,
|
||||||
|
height: mainWindowState.height,
|
||||||
|
minWidth: 1080,
|
||||||
|
minHeight: 600,
|
||||||
|
show: true,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
transparent: process.platform === 'darwin',
|
||||||
|
vibrancy: 'fullscreen-ui',
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||||
|
trafficLightPosition: { x: 8, y: 12 },
|
||||||
|
...(process.platform === 'linux' ? { icon } : {}),
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false,
|
||||||
|
webSecurity: false,
|
||||||
|
webviewTag: true
|
||||||
|
// devTools: !app.isPackaged,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindowState.manage(mainWindow)
|
||||||
|
|
||||||
|
mainWindow.webContents.on('context-menu', () => {
|
||||||
|
const menu = new Menu()
|
||||||
|
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
|
||||||
|
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
|
||||||
|
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
|
||||||
|
menu.append(new MenuItem({ type: 'separator' }))
|
||||||
|
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
|
||||||
|
menu.popup()
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
mainWindow.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||||
|
event.preventDefault()
|
||||||
|
shell.openExternal(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url)
|
||||||
|
return { action: 'deny' }
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
|
||||||
|
if (details.responseHeaders?.['X-Frame-Options']) {
|
||||||
|
delete details.responseHeaders['X-Frame-Options']
|
||||||
|
}
|
||||||
|
if (details.responseHeaders?.['x-frame-options']) {
|
||||||
|
delete details.responseHeaders['x-frame-options']
|
||||||
|
}
|
||||||
|
if (details.responseHeaders?.['Content-Security-Policy']) {
|
||||||
|
delete details.responseHeaders['Content-Security-Policy']
|
||||||
|
}
|
||||||
|
if (details.responseHeaders?.['content-security-policy']) {
|
||||||
|
delete details.responseHeaders['content-security-policy']
|
||||||
|
}
|
||||||
|
callback({ cancel: false, responseHeaders: details.responseHeaders })
|
||||||
|
})
|
||||||
|
|
||||||
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMinappWindow({
|
||||||
|
url,
|
||||||
|
parent,
|
||||||
|
windowOptions
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
parent?: BrowserWindow
|
||||||
|
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||||
|
}) {
|
||||||
|
const width = windowOptions?.width || 1000
|
||||||
|
const height = windowOptions?.height || 680
|
||||||
|
|
||||||
|
const minappWindow = new BrowserWindow({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
title: 'Cherry Studio',
|
||||||
|
...windowOptions,
|
||||||
|
parent,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/minapp.js'),
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
minappWindow.loadURL(url)
|
||||||
|
|
||||||
|
return minappWindow
|
||||||
|
}
|
||||||
22
src/preload/index.d.ts
vendored
@@ -1,4 +1,6 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
|
import { FileType } from '@renderer/types'
|
||||||
|
import type { OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -7,9 +9,29 @@ declare global {
|
|||||||
getAppInfo: () => Promise<{
|
getAppInfo: () => Promise<{
|
||||||
version: string
|
version: string
|
||||||
isPackaged: boolean
|
isPackaged: boolean
|
||||||
|
appPath: string
|
||||||
}>
|
}>
|
||||||
checkForUpdate: () => void
|
checkForUpdate: () => void
|
||||||
openWebsite: (url: string) => void
|
openWebsite: (url: string) => void
|
||||||
|
setProxy: (proxy: string | undefined) => void
|
||||||
|
saveFile: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
|
||||||
|
openFile: (options?: OpenDialogOptions) => Promise<{ fileName: string; content: Buffer } | null>
|
||||||
|
setTheme: (theme: 'light' | 'dark') => void
|
||||||
|
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||||
|
reload: () => void
|
||||||
|
compress: (text: string) => Promise<Buffer>
|
||||||
|
decompress: (text: Buffer) => Promise<string>
|
||||||
|
file: {
|
||||||
|
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||||
|
upload: (file: FileType) => Promise<FileType>
|
||||||
|
delete: (fileId: string) => Promise<void>
|
||||||
|
read: (fileId: string) => Promise<string>
|
||||||
|
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||||
|
clear: () => Promise<void>
|
||||||
|
get: (filePath: string) => Promise<FileType | null>
|
||||||
|
create: (fileName: string) => Promise<string>
|
||||||
|
write: (filePath: string, data: Uint8Array | string) => Promise<void>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
import { contextBridge, ipcRenderer } from 'electron'
|
|
||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
|
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
|
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
|
||||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url)
|
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||||
|
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||||
|
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
|
||||||
|
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||||
|
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
|
||||||
|
reload: () => ipcRenderer.invoke('reload'),
|
||||||
|
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
|
||||||
|
return ipcRenderer.invoke('save-file', path, content, options)
|
||||||
|
},
|
||||||
|
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||||
|
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text),
|
||||||
|
file: {
|
||||||
|
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||||
|
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
||||||
|
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId),
|
||||||
|
read: (fileId: string) => ipcRenderer.invoke('file:read', fileId),
|
||||||
|
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
|
||||||
|
clear: () => ipcRenderer.invoke('file:clear'),
|
||||||
|
get: (filePath: string) => ipcRenderer.invoke('file:get', filePath),
|
||||||
|
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
|
||||||
|
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
@@ -2,15 +2,12 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Cherry Studio</title>
|
|
||||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; connect-src *; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' *; img-src 'self' data:" />
|
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
|
||||||
</head>
|
</head>
|
||||||
|
<body>
|
||||||
<body theme-mode="dark">
|
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,34 +1,44 @@
|
|||||||
import '@fontsource/inter'
|
import '@renderer/databases'
|
||||||
|
|
||||||
import store, { persistor } from '@renderer/store'
|
import store, { persistor } from '@renderer/store'
|
||||||
import { ConfigProvider } from 'antd'
|
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
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 { AntdThemeConfig, getAntdLocale } from './config/antd'
|
import AntdProvider from './context/AntdProvider'
|
||||||
|
import { ThemeProvider } from './context/ThemeProvider'
|
||||||
|
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 HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={AntdThemeConfig} locale={getAntdLocale()}>
|
<Provider store={store}>
|
||||||
<Provider store={store}>
|
<ThemeProvider>
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
<AntdProvider>
|
||||||
<TopViewContainer>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<HashRouter>
|
<TopViewContainer>
|
||||||
<Sidebar />
|
<HashRouter>
|
||||||
<Routes>
|
<Sidebar />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Routes>
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/files" element={<FilesPage />} />
|
||||||
</Routes>
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
</HashRouter>
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
</TopViewContainer>
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
</PersistGate>
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
</Provider>
|
</Routes>
|
||||||
</ConfigProvider>
|
</HashRouter>
|
||||||
|
</TopViewContainer>
|
||||||
|
</PersistGate>
|
||||||
|
</AntdProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# CHANGES LOG
|
|
||||||
|
|
||||||
### v0.3.0 - 2024-07-21
|
|
||||||
|
|
||||||
- Supports setting the model Temperature parameter
|
|
||||||
- Support for setting the number of contexts
|
|
||||||
- Token consumption estimation added to the input box
|
|
||||||
|
|
||||||
### v0.2.9 - 2024-07-20
|
|
||||||
|
|
||||||
- 📢 Add AiHubMix provider
|
|
||||||
|
|
||||||
### v0.2.8 - 2024-07-20
|
|
||||||
|
|
||||||
- 🆕 Feature: Add customized service providers
|
|
||||||
|
|
||||||
### v0.2.7 - 2024-07-19
|
|
||||||
|
|
||||||
- 📢 Add DashScope Provider
|
|
||||||
- 📢 Add Anthropic Provider
|
|
||||||
|
|
||||||
### v0.2.6 - 2024-07-17
|
|
||||||
|
|
||||||
- 🆕 Fixed the issue of the BaiChuan API KEY not displaying when clicking to obtain the URL
|
|
||||||
- 📢 New intelligent body center style
|
|
||||||
|
|
||||||
### v0.2.5 - 2024-07-17
|
|
||||||
|
|
||||||
- 🆕 Baichuan AI Service Providers
|
|
||||||
- 📢 New Intelligent Agent Page with Multiple Professional Assistants
|
|
||||||
- 🌐 Multilingual Issue Fixes and Detailed Optimizations
|
|
||||||
|
|
||||||
### v0.2.4 - 2024-07-16
|
|
||||||
|
|
||||||
- Fixed the issue of the update log page cannot be scrolled
|
|
||||||
- Added a check for updates button
|
|
||||||
|
|
||||||
### v0.2.3 - 2024-07-16
|
|
||||||
|
|
||||||
- Fixed multi-language prompt errors
|
|
||||||
- Fixed default model error issues with ZHIPU AI
|
|
||||||
- Fixed OpenRouter API detection error issues
|
|
||||||
- Fixed multi-language translation errors with model providers
|
|
||||||
|
|
||||||
### v0.2.2 - 2024-07-15
|
|
||||||
|
|
||||||
- Fix the issue where the default assistant name is empty.
|
|
||||||
- Fix the problem with default language detection during the first installation.
|
|
||||||
- Adjust the changelog style.
|
|
||||||
|
|
||||||
### v0.2.1 - 2024-07-15
|
|
||||||
|
|
||||||
- **Feature**: Add new feature for pausing message sending
|
|
||||||
- **Fix**: Resolve incomplete translation issue upon language switch
|
|
||||||
- **Build**: Support for macOS Intel architecture
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# 更新日志
|
|
||||||
|
|
||||||
### v0.3.0 - 2024-07-21
|
|
||||||
|
|
||||||
- 支持设置模型 Temperature 参数
|
|
||||||
- 支持设置上下文数量
|
|
||||||
- 输入框增加 Token 消耗预估
|
|
||||||
|
|
||||||
### v0.2.9 - 2024-07-20
|
|
||||||
|
|
||||||
- 📢 新增 AiMixHub 服务提供商
|
|
||||||
|
|
||||||
### v0.2.8 - 2024-07-20
|
|
||||||
|
|
||||||
- 🆕 新功能: 可以添加自定义服务提供商了
|
|
||||||
|
|
||||||
### v0.2.7 - 2024-07-19
|
|
||||||
|
|
||||||
- 📢 新增阿里云灵积服务商
|
|
||||||
- 📢 新增 Anthropic 服务商
|
|
||||||
|
|
||||||
### v0.2.6 - 2024-07-17
|
|
||||||
|
|
||||||
- 🆕 修复百川 API KEY 点击获取网址没有显示问题
|
|
||||||
- 📢 新的智能体中心样式
|
|
||||||
|
|
||||||
### v0.2.5 - 2024-07-17
|
|
||||||
|
|
||||||
- 🆕 新增百川AI服务商
|
|
||||||
- 📢 全新的智能体页面,新增多种职业助手
|
|
||||||
- 🌐 多语言问题修复,细节优化
|
|
||||||
|
|
||||||
### v0.2.4 - 2024-07-16
|
|
||||||
|
|
||||||
- 修复更新日志页面不能滚动问题
|
|
||||||
- 新增检查更新按钮
|
|
||||||
|
|
||||||
### v0.2.3 - 2024-07-16
|
|
||||||
|
|
||||||
- 修复多语言提示错误
|
|
||||||
- 修复智谱AI默认模型错误问题
|
|
||||||
- 修复 OpenRouter API 检测出错问题
|
|
||||||
- 修复模型提供商多语言翻译错误问题
|
|
||||||
|
|
||||||
### v0.2.2 - 2024-07-15
|
|
||||||
|
|
||||||
- 修复默认助理名称为空的问题
|
|
||||||
- 修复首次安装默认语言检测问题
|
|
||||||
- 更新日志样式微调
|
|
||||||
|
|
||||||
### v0.2.1 - 2024-07-15
|
|
||||||
|
|
||||||
- 【功能】新增消息暂停发送功能
|
|
||||||
- 【修复】修复多语言切换不彻底问题
|
|
||||||
- 【构建】支持 macOS Intel 架构
|
|
||||||
|
|
||||||
88
src/renderer/src/assets/fonts/icon-fonts/iconfont.css
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'iconfont'; /* Project id 4563475 */
|
||||||
|
src: url('iconfont.woff2?t=1725606177995') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-family: 'iconfont' !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-a-darkmode:before {
|
||||||
|
content: '\e6cd';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ai-model:before {
|
||||||
|
content: '\e827';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ai-model1:before {
|
||||||
|
content: '\ec09';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gridlines:before {
|
||||||
|
content: '\e942';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-row-2copy:before {
|
||||||
|
content: '\e681';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-inbox:before {
|
||||||
|
content: '\e869';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-business-smart-assistant:before {
|
||||||
|
content: '\e601';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-copy:before {
|
||||||
|
content: '\e6ae';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ic_send:before {
|
||||||
|
content: '\e795';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dark1:before {
|
||||||
|
content: '\e72f';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-theme-light:before {
|
||||||
|
content: '\e6b7';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-translate_line:before {
|
||||||
|
content: '\e7de';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-history:before {
|
||||||
|
content: '\e758';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-hide-sidebar:before {
|
||||||
|
content: '\e8eb';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-show-sidebar:before {
|
||||||
|
content: '\e944';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-a-addchat:before {
|
||||||
|
content: '\e658';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-appstore:before {
|
||||||
|
content: '\e792';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chat:before {
|
||||||
|
content: '\e615';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-setting:before {
|
||||||
|
content: '\e78e';
|
||||||
|
}
|
||||||
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Bold.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-BoldItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Italic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Light.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-LightItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Medium.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-MediumItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Regular.ttf
Normal file
55
src/renderer/src/assets/fonts/ubuntu/ubuntu.css
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
src: url('Ubuntu-Regular.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
src: url('Ubuntu-Italic.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
src: url('Ubuntu-Bold.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
src: url('Ubuntu-BoldItalic.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
src: url('Ubuntu-Light.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
src: url('Ubuntu-LightItalic.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
src: url('Ubuntu-Medium.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
src: url('Ubuntu-MediumItalic.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
BIN
src/renderer/src/assets/images/apps/360-ai.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/renderer/src/assets/images/apps/ai-search.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/renderer/src/assets/images/apps/baidu-ai.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/renderer/src/assets/images/apps/baixiaoying.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/renderer/src/assets/images/apps/devv.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/renderer/src/assets/images/apps/kimi.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/renderer/src/assets/images/apps/metaso.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/renderer/src/assets/images/apps/perplexity.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/apps/poe.webp
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/renderer/src/assets/images/apps/sensetime.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/renderer/src/assets/images/apps/sparkdesk.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/apps/tiangong.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/renderer/src/assets/images/apps/yuanbao.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/apps/yuewen.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/renderer/src/assets/images/apps/zhihu.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/avatar.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/avatar.webp
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
BIN
src/renderer/src/assets/images/models/chatglm.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
BIN
src/renderer/src/assets/images/models/cohere.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/renderer/src/assets/images/models/doubao.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/models/embedding.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/renderer/src/assets/images/models/gemini.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/renderer/src/assets/images/models/hailuo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/models/minicpm.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
67
src/renderer/src/assets/images/models/palm.svg
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Standard_product_icon__x28_1:1_x29_"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="192px" height="192px"
|
||||||
|
viewBox="0 0 192 192" enable-background="new 0 0 192 192" xml:space="preserve">
|
||||||
|
<symbol id="material_x5F_product_x5F_standard_x5F_icon_x5F_keylines_00000077318920148093339210000006245950728745084294_" viewBox="-96 -96 192 192">
|
||||||
|
<g opacity="0.4">
|
||||||
|
<defs>
|
||||||
|
<path id="SVGID_1_" opacity="0.4" d="M-96,96V-96H96V96H-96z"/>
|
||||||
|
</defs>
|
||||||
|
<clipPath id="SVGID_00000071517564283228984050000017848131202901217410_">
|
||||||
|
<use xlink:href="#SVGID_1_" overflow="visible"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)">
|
||||||
|
<g>
|
||||||
|
<path d="M95.75,95.75v-191.5h-191.5v191.5H95.75 M96,96H-96V-96H96V96L96,96z"/>
|
||||||
|
</g>
|
||||||
|
<circle fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" cx="0" cy="0" r="64"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<circle clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" cx="0" cy="0" r="88"/>
|
||||||
|
|
||||||
|
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
|
||||||
|
M64,76H-64c-6.6,0-12-5.4-12-12V-64c0-6.6,5.4-12,12-12H64c6.6,0,12,5.4,12,12V64C76,70.6,70.6,76,64,76z"/>
|
||||||
|
|
||||||
|
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
|
||||||
|
M52,88H-52c-6.6,0-12-5.4-12-12V-76c0-6.6,5.4-12,12-12H52c6.6,0,12,5.4,12,12V76C64,82.6,58.6,88,52,88z"/>
|
||||||
|
|
||||||
|
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
|
||||||
|
M76,64H-76c-6.6,0-12-5.4-12-12V-52c0-6.6,5.4-12,12-12H76c6.6,0,12,5.4,12,12V52C88,58.6,82.6,64,76,64z"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<rect id="bounding_box_1_" display="none" fill="none" width="192" height="192"/>
|
||||||
|
<g id="art_layer">
|
||||||
|
<g>
|
||||||
|
<path fill="#F9AB00" d="M96,181.92L96,181.92c6.63,0,12-5.37,12-12v-104H84v104C84,176.55,89.37,181.92,96,181.92z"/>
|
||||||
|
<g>
|
||||||
|
<path fill="#5BB974" d="M143.81,103.87C130.87,90.94,111.54,88.32,96,96l51.37,51.37c2.12,2.12,5.77,1.28,6.67-1.57
|
||||||
|
C158.56,131.49,155.15,115.22,143.81,103.87z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#129EAF" d="M48.19,103.87C61.13,90.94,80.46,88.32,96,96l-51.37,51.37c-2.12,2.12-5.77,1.28-6.67-1.57
|
||||||
|
C33.44,131.49,36.85,115.22,48.19,103.87z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#AF5CF7" d="M140,64c-20.44,0-37.79,13.4-44,32h81.24c3.33,0,5.55-3.52,4.04-6.49C173.56,74.36,157.98,64,140,64z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#FF8BCB" d="M104.49,42.26C90.03,56.72,87.24,78.45,96,96l57.45-57.45c2.36-2.36,1.44-6.42-1.73-7.45
|
||||||
|
C135.54,25.85,117.2,29.55,104.49,42.26z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#FA7B17" d="M87.51,42.26C101.97,56.72,104.76,78.45,96,96L38.55,38.55c-2.36-2.36-1.44-6.42,1.73-7.45
|
||||||
|
C56.46,25.85,74.8,29.55,87.51,42.26z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path fill="#4285F4" d="M52,64c20.44,0,37.79,13.4,44,32H14.76c-3.33,0-5.55-3.52-4.04-6.49C18.44,74.36,34.02,64,52,64z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="keylines" display="none">
|
||||||
|
|
||||||
|
<use xlink:href="#material_x5F_product_x5F_standard_x5F_icon_x5F_keylines_00000077318920148093339210000006245950728745084294_" width="192" height="192" id="material_x5F_product_x5F_standard_x5F_icon_x5F_keylines" x="-96" y="-96" transform="matrix(1 0 0 -1 96 96)" display="inline" overflow="visible"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
BIN
src/renderer/src/assets/images/models/qwen.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/renderer/src/assets/images/models/step.jpg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src/renderer/src/assets/images/models/yi.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,7 +0,0 @@
|
|||||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="300" cy="300" r="300" fill="white"/>
|
|
||||||
<rect x="409.733" y="340.032" width="42.3862" height="151.648" rx="21.1931" fill="#003425"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M422.005 133.354C413.089 125.771 399.714 126.851 392.131 135.768L273.699 275.021C270.643 278.614 268.994 282.932 268.698 287.302C268.532 288.371 268.446 289.466 268.446 290.581V468.603C268.446 480.308 277.934 489.796 289.639 489.796C301.344 489.796 310.832 480.308 310.832 468.603V296.784L424.419 163.228C432.002 154.312 430.921 140.937 422.005 133.354Z" fill="#003425"/>
|
|
||||||
<rect x="113.972" y="134.25" width="42.3862" height="174.745" rx="21.1931" transform="rotate(-39.3441 113.972 134.25)" fill="#003425"/>
|
|
||||||
<circle cx="460.126" cy="279.278" r="25.9027" fill="#00DD20"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 869 B |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
src/renderer/src/assets/images/providers/doubao.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/providers/gemini.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
3
src/renderer/src/assets/images/providers/github.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 0C7.16 0 0 7.16 0 16C0 23.08 4.58 29.06 10.94 31.18C11.74 31.32 12.04 30.84 12.04 30.42C12.04 30.04 12.02 28.78 12.02 27.44C8 28.18 6.96 26.46 6.64 25.56C6.46 25.1 5.68 23.68 5 23.3C4.44 23 3.64 22.26 4.98 22.24C6.24 22.22 7.14 23.4 7.44 23.88C8.88 26.3 11.18 25.62 12.1 25.2C12.24 24.16 12.66 23.46 13.12 23.06C9.56 22.66 5.84 21.28 5.84 15.16C5.84 13.42 6.46 11.98 7.48 10.86C7.32 10.46 6.76 8.82 7.64 6.62C7.64 6.62 8.98 6.2 12.04 8.26C13.32 7.9 14.68 7.72 16.04 7.72C17.4 7.72 18.76 7.9 20.04 8.26C23.1 6.18 24.44 6.62 24.44 6.62C25.32 8.82 24.76 10.46 24.6 10.86C25.62 11.98 26.24 13.4 26.24 15.16C26.24 21.3 22.5 22.66 18.94 23.06C19.52 23.56 20.02 24.52 20.02 26.02C20.02 28.16 20 29.88 20 30.42C20 30.84 20.3 31.34 21.1 31.18C27.42 29.06 32 23.06 32 16C32 7.16 24.84 0 16 0V0Z" fill="#24292E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 959 B |
BIN
src/renderer/src/assets/images/providers/graph-rag.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/providers/graph-rag.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/renderer/src/assets/images/providers/minimax.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
BIN
src/renderer/src/assets/images/providers/moonshot.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/providers/openai.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/renderer/src/assets/images/providers/stepfun.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/renderer/src/assets/images/providers/yi.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,7 +0,0 @@
|
|||||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="300" cy="300" r="300" fill="#003425"/>
|
|
||||||
<rect x="409.733" y="340.031" width="42.3862" height="151.648" rx="21.1931" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M422.005 133.354C413.089 125.771 399.714 126.851 392.131 135.767L273.699 275.021C270.643 278.614 268.994 282.932 268.698 287.302C268.532 288.371 268.446 289.466 268.446 290.581V468.603C268.446 480.308 277.934 489.796 289.639 489.796C301.344 489.796 310.832 480.308 310.832 468.603V296.784L424.419 163.228C432.002 154.312 430.921 140.937 422.005 133.354Z" fill="white"/>
|
|
||||||
<rect x="113.972" y="134.25" width="42.3862" height="174.745" rx="21.1931" transform="rotate(-39.3441 113.972 134.25)" fill="white"/>
|
|
||||||
<circle cx="460.126" cy="279.278" r="25.9027" fill="#00FF25"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 865 B |
@@ -1,10 +1,7 @@
|
|||||||
@import 'https://at.alicdn.com/t/c/font_4563475_hrx8c92awui.css';
|
|
||||||
@import './markdown.scss';
|
@import './markdown.scss';
|
||||||
|
@import './scrollbar.scss';
|
||||||
// @font-face {
|
@import '../fonts/icon-fonts/iconfont.css';
|
||||||
// font-family: 'Playwrite';
|
@import '../fonts/ubuntu/ubuntu.css';
|
||||||
// src: url(../fonts/Playwrite.ttf) format('truetype');
|
|
||||||
// }
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-white: #ffffff;
|
--color-white: #ffffff;
|
||||||
@@ -12,7 +9,7 @@
|
|||||||
--color-white-mute: #f2f2f2;
|
--color-white-mute: #f2f2f2;
|
||||||
|
|
||||||
--color-black: #1b1b1f;
|
--color-black: #1b1b1f;
|
||||||
--color-black-soft: #303030;
|
--color-black-soft: #262626;
|
||||||
--color-black-mute: #363636;
|
--color-black-mute: #363636;
|
||||||
|
|
||||||
--color-gray-1: #515c67;
|
--color-gray-1: #515c67;
|
||||||
@@ -27,18 +24,74 @@
|
|||||||
--color-background-soft: var(--color-black-soft);
|
--color-background-soft: var(--color-black-soft);
|
||||||
--color-background-mute: var(--color-black-mute);
|
--color-background-mute: var(--color-black-mute);
|
||||||
|
|
||||||
|
--color-primary: #00b96b;
|
||||||
|
--color-primary-soft: #00b96b99;
|
||||||
|
--color-primary-mute: #00b96b33;
|
||||||
|
|
||||||
--color-text: var(--color-text-1);
|
--color-text: var(--color-text-1);
|
||||||
--color-icon: #ffffff99;
|
--color-icon: #ffffff99;
|
||||||
--color-icon-white: #ffffff;
|
--color-icon-white: #ffffff;
|
||||||
--color-border: #ffffff20;
|
--color-border: #ffffff20;
|
||||||
|
--color-border-soft: #ffffff20;
|
||||||
|
--color-error: #f44336;
|
||||||
|
--color-link: #1677ff;
|
||||||
|
--color-code-background: #323232;
|
||||||
|
--color-scrollbar-thumb: rgba(255, 255, 255, 0.08);
|
||||||
|
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15);
|
||||||
|
|
||||||
|
--navbar-background-mac: rgba(30, 30, 30, 0.8);
|
||||||
|
--navbar-background: rgba(30, 30, 30);
|
||||||
|
--input-bar-background: rgba(255, 255, 255, 0.02);
|
||||||
|
|
||||||
--navbar-height: 42px;
|
--navbar-height: 42px;
|
||||||
--sidebar-width: 55px;
|
--sidebar-width: 52px;
|
||||||
--assistants-width: 235px;
|
|
||||||
--topic-list-width: 250px;
|
|
||||||
--settings-width: var(--assistants-width);
|
|
||||||
--status-bar-height: 40px;
|
--status-bar-height: 40px;
|
||||||
--input-bar-height: 125px;
|
--input-bar-height: 85px;
|
||||||
|
|
||||||
|
--assistants-width: 280px;
|
||||||
|
--topic-list-width: 280px;
|
||||||
|
--settings-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[theme-mode='light'] {
|
||||||
|
--color-white: #ffffff;
|
||||||
|
--color-white-soft: #f8f8f8;
|
||||||
|
--color-white-mute: #efefef;
|
||||||
|
|
||||||
|
--color-black: #1b1b1f;
|
||||||
|
--color-black-soft: #262626;
|
||||||
|
--color-black-mute: #363636;
|
||||||
|
|
||||||
|
--color-gray-1: #8e8e93;
|
||||||
|
--color-gray-2: #aeaeb2;
|
||||||
|
--color-gray-3: #c7c7cc;
|
||||||
|
|
||||||
|
--color-text-1: rgba(0, 0, 0, 1);
|
||||||
|
--color-text-2: rgba(0, 0, 0, 0.6);
|
||||||
|
--color-text-3: rgba(0, 0, 0, 0.38);
|
||||||
|
|
||||||
|
--color-background: #ffffff;
|
||||||
|
--color-background-soft: var(--color-white-soft);
|
||||||
|
--color-background-mute: var(--color-white-mute);
|
||||||
|
|
||||||
|
--color-primary: #00b96b;
|
||||||
|
--color-primary-soft: #00b96b99;
|
||||||
|
--color-primary-mute: #00b96b33;
|
||||||
|
|
||||||
|
--color-text: var(--color-text-1);
|
||||||
|
--color-icon: #00000099;
|
||||||
|
--color-icon-white: #000000;
|
||||||
|
--color-border: #00000028;
|
||||||
|
--color-border-soft: #00000028;
|
||||||
|
--color-error: #f44336;
|
||||||
|
--color-link: #1677ff;
|
||||||
|
--color-code-background: #e3e3e3;
|
||||||
|
--color-scrollbar-thumb: rgba(0, 0, 0, 0.08);
|
||||||
|
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
--navbar-background-mac: rgba(255, 255, 255, 0.75);
|
||||||
|
--navbar-background: rgba(255, 255, 255);
|
||||||
|
--input-bar-background: rgba(0, 0, 0, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -49,6 +102,14 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
@@ -57,23 +118,12 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background: var(--color-background);
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-size: cover;
|
background: transparent !important;
|
||||||
font-family:
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||||
Inter,
|
'Helvetica Neue', sans-serif;
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Fira Sans',
|
|
||||||
'Droid Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
@@ -102,29 +152,88 @@ body,
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 全局初始化滚动条样式 */
|
.chat-nav-dropdown {
|
||||||
::-webkit-scrollbar {
|
.ant-dropdown-menu {
|
||||||
width: 5px;
|
padding-bottom: 12px;
|
||||||
height: 5px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
.loader {
|
||||||
background: transparent;
|
width: 16px;
|
||||||
}
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
::-webkit-scrollbar-thumb {
|
background-color: #000;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
box-shadow:
|
||||||
border-radius: 10px;
|
32px 0 #000,
|
||||||
}
|
-32px 0 #000;
|
||||||
|
position: relative;
|
||||||
::-webkit-scrollbar-thumb:hover {
|
animation: flash 0.5s ease-out infinite alternate;
|
||||||
background: rgba(255, 255, 255, 0.4);
|
}
|
||||||
}
|
|
||||||
|
.ant-segmented-group {
|
||||||
/* Safari 和 Chrome */
|
gap: 4px;
|
||||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
}
|
||||||
body {
|
|
||||||
scrollbar-width: thin; /* 告诉 FF 用细滚动条 */
|
.drag {
|
||||||
scrollbar-color: rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.1); /* FF 前面色后面色 */
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodrag {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minapp-drawer {
|
||||||
|
.ant-drawer-content-wrapper {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.ant-drawer-header {
|
||||||
|
position: absolute;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
min-height: calc(var(--navbar-height) + 0.5px);
|
||||||
|
background: var(--navbar-background);
|
||||||
|
width: calc(100vw - var(--sidebar-width));
|
||||||
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
|
margin-top: -0.5px;
|
||||||
|
}
|
||||||
|
.ant-drawer-body {
|
||||||
|
padding: 0;
|
||||||
|
margin-top: var(--navbar-height);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.minapp-mask {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-header {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-tab {
|
||||||
|
.ant-segmented-item-label {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.iconfont {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
.anticon-setting {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.icon-business-smart-assistant {
|
||||||
|
margin-right: -2px;
|
||||||
|
}
|
||||||
|
.ant-segmented-item-icon + * {
|
||||||
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
.markdown {
|
.markdown {
|
||||||
color: #fff;
|
color: var(--color-text);
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
margin-top: 4px;
|
word-break: break-word;
|
||||||
|
|
||||||
p:first-of-type {
|
h1:first-child,
|
||||||
margin-top: 0;
|
h2:first-child,
|
||||||
}
|
h3:first-child,
|
||||||
|
h4:first-child,
|
||||||
h1:first-of-type,
|
h5:first-child,
|
||||||
h2:first-of-type,
|
h6:first-child {
|
||||||
h3:first-of-type,
|
|
||||||
h4:first-of-type,
|
|
||||||
h5:first-of-type,
|
|
||||||
h6:first-of-type {
|
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,62 +21,238 @@
|
|||||||
h6 {
|
h6 {
|
||||||
margin: 1em 0 1em 0;
|
margin: 1em 0 1em 0;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||||
|
'Helvetica Neue', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
color: #fff;
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
|
padding-bottom: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
color: #fff;
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
|
padding-bottom: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
color: #fff;
|
&:last-child {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
padding-left: 1.5em;
|
padding-left: 1.5em;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
color: #ccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
|
pre {
|
||||||
|
margin: 1.5em 0;
|
||||||
|
}
|
||||||
|
&::marker {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li > ul,
|
||||||
|
li > ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid #555;
|
border-top: 0.5px solid var(--color-border);
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
background-color: #555;
|
background-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
p code,
|
||||||
|
li code {
|
||||||
|
background: var(--color-background-mute);
|
||||||
|
padding: 3px 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
&:not(pre pre) {
|
||||||
|
> code:not(pre pre > code) {
|
||||||
|
padding: 15px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 0 !important;
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre + pre {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding-left: 1em;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
border-left: 4px solid var(--color-border);
|
||||||
|
font-family: Georgia, 'Times New Roman', Times, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1em 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||||
|
'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.link {
|
||||||
|
color: var(--color-link);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
del {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup,
|
||||||
|
sub {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footnote-ref {
|
||||||
|
font-size: 0.8em;
|
||||||
|
vertical-align: super;
|
||||||
|
line-height: 0;
|
||||||
|
margin: 0 2px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footnotes {
|
||||||
|
margin-top: 1em;
|
||||||
|
padding-top: 1em;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
|
||||||
|
ol {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footnote-backref {
|
||||||
|
font-size: 0.8em;
|
||||||
|
vertical-align: super;
|
||||||
|
line-height: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emoji-picker {
|
||||||
|
--border-size: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/renderer/src/assets/styles/scrollbar.scss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* 全局初始化滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-scrollbar-thumb);
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-scrollbar-thumb-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/renderer/src/components/Avatar/ModelAvatar.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { getModelLogo } from '@renderer/config/provider'
|
||||||
|
import { Model } from '@renderer/types'
|
||||||
|
import { Avatar, AvatarProps } from 'antd'
|
||||||
|
import { first } from 'lodash'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
model: Model
|
||||||
|
size: number
|
||||||
|
props?: AvatarProps
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModelAvatar: FC<Props> = ({ model, size, props }) => {
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
src={getModelLogo(model?.id || '')}
|
||||||
|
style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
{...props}>
|
||||||
|
{first(model?.name)}
|
||||||
|
</Avatar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModelAvatar
|
||||||
70
src/renderer/src/components/DragableList/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Draggable,
|
||||||
|
Droppable,
|
||||||
|
DroppableProps,
|
||||||
|
DropResult,
|
||||||
|
OnDragEndResponder,
|
||||||
|
OnDragStartResponder,
|
||||||
|
ResponderProvided
|
||||||
|
} from '@hello-pangea/dnd'
|
||||||
|
import { droppableReorder } from '@renderer/utils'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
interface Props<T> {
|
||||||
|
list: T[]
|
||||||
|
style?: React.CSSProperties
|
||||||
|
listStyle?: React.CSSProperties
|
||||||
|
children: (item: T, index: number) => React.ReactNode
|
||||||
|
onUpdate: (list: T[]) => void
|
||||||
|
onDragStart?: OnDragStartResponder
|
||||||
|
onDragEnd?: OnDragEndResponder
|
||||||
|
droppableProps?: Partial<DroppableProps>
|
||||||
|
}
|
||||||
|
|
||||||
|
const DragableList: FC<Props<any>> = ({
|
||||||
|
children,
|
||||||
|
list,
|
||||||
|
style,
|
||||||
|
listStyle,
|
||||||
|
droppableProps,
|
||||||
|
onDragStart,
|
||||||
|
onUpdate,
|
||||||
|
onDragEnd
|
||||||
|
}) => {
|
||||||
|
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||||
|
onDragEnd?.(result, provided)
|
||||||
|
if (result.destination) {
|
||||||
|
const sourceIndex = result.source.index
|
||||||
|
const destIndex = result.destination.index
|
||||||
|
const reorderAgents = droppableReorder(list, sourceIndex, destIndex)
|
||||||
|
onUpdate(reorderAgents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
||||||
|
<Droppable droppableId="droppable" {...droppableProps}>
|
||||||
|
{(provided) => (
|
||||||
|
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index} {...droppableProps}>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
|
||||||
|
{children(item, index)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DragableList
|
||||||
25
src/renderer/src/components/EmojiPicker/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { FC, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onEmojiClick: (emoji: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.addEventListener('emoji-click', (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
onEmojiClick(event.detail.emoji.unicode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [onEmojiClick])
|
||||||
|
|
||||||
|
// @ts-ignore next-line
|
||||||
|
return <emoji-picker ref={ref} class={theme === 'dark' ? 'dark' : 'light'} style={{ border: 'none' }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmojiPicker
|
||||||
7
src/renderer/src/components/Icons/CopyIcon.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
const CopyIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||||
|
return <i {...props} className={`iconfont icon-copy ${props.className}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyIcon
|
||||||
15
src/renderer/src/components/Icons/VisionIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { EyeOutlined } from '@ant-design/icons'
|
||||||
|
import React, { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const VisionIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||||
|
return <Icon {...(props as any)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = styled(EyeOutlined)`
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 4px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default VisionIcon
|
||||||
@@ -150,11 +150,12 @@ export const BaseTypography = styled(Box)<{
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const TypographyNormal = styled(BaseTypography)`
|
export const TypographyNormal = styled(BaseTypography)`
|
||||||
font-family: 'Poppins';
|
font-family: 'Ubuntu';
|
||||||
`
|
`
|
||||||
|
|
||||||
export const TypographyBold = styled(BaseTypography)`
|
export const TypographyBold = styled(BaseTypography)`
|
||||||
font-family: 'Poppins Bold';
|
font-family: 'Ubuntu';
|
||||||
|
font-weight: bold;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Container = styled.main<ContainerProps>`
|
export const Container = styled.main<ContainerProps>`
|
||||||
|
|||||||