Compare commits
373 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27631d9cff | ||
|
|
596cf8e3f2 | ||
|
|
6e2ab66b81 | ||
|
|
2cbb4c8831 | ||
|
|
5347f63aa8 | ||
|
|
077a66c675 | ||
|
|
6e7b6d8387 | ||
|
|
bdf6df1936 | ||
|
|
a2dd440f77 | ||
|
|
b47d6c95e7 | ||
|
|
6265d27ebc | ||
|
|
0dd60cb129 | ||
|
|
04dae10d89 | ||
|
|
71ef0f319f | ||
|
|
58817ae82f | ||
|
|
7e477cb9c7 | ||
|
|
1063610c01 | ||
|
|
927670d3a3 | ||
|
|
2fea7659b1 | ||
|
|
43b9298329 | ||
|
|
fe2e3bfc36 | ||
|
|
bae80fda8d | ||
|
|
ab709b9c61 | ||
|
|
2c28e3bb76 | ||
|
|
d98020e12c | ||
|
|
25addc390f | ||
|
|
88d04a1a6e | ||
|
|
1f582c672d | ||
|
|
c913b2a6d0 | ||
|
|
267c60f24d | ||
|
|
a8ccaf6847 | ||
|
|
a3a005b946 | ||
|
|
2220a6016e | ||
|
|
3197390f1a | ||
|
|
5f04d1adb1 | ||
|
|
76b6593545 | ||
|
|
04ce641bf7 | ||
|
|
31e912aac3 | ||
|
|
832ec99d92 | ||
|
|
ef9fda6d0c | ||
|
|
624230411a | ||
|
|
14808649f8 | ||
|
|
3cc8cfb43b | ||
|
|
4055111ade | ||
|
|
dc98b27e3e | ||
|
|
90fec317e5 | ||
|
|
303a0e20a0 | ||
|
|
d69252a7da | ||
|
|
99f05383cb | ||
|
|
5ba6c9f882 | ||
|
|
27f64409d6 | ||
|
|
7237729ff6 | ||
|
|
d29cd3c657 | ||
|
|
8c87f59822 | ||
|
|
5780141df4 | ||
|
|
f5799ef47b | ||
|
|
6f502049f4 | ||
|
|
c68ad4febb | ||
|
|
2ebcec9f59 | ||
|
|
7bc74a5b86 | ||
|
|
75152421d9 | ||
|
|
3326074076 | ||
|
|
362d82bdcc | ||
|
|
fcce241c82 | ||
|
|
693b06c126 | ||
|
|
c310c71576 | ||
|
|
bea95fc52f | ||
|
|
969cf8ea21 | ||
|
|
5b357f14e5 | ||
|
|
de5db4f805 | ||
|
|
1ccb5edda7 | ||
|
|
97b8749dd1 | ||
|
|
a6d7ecae81 | ||
|
|
938efb5aef | ||
|
|
9baf0f772e | ||
|
|
ff5de3625e | ||
|
|
f1cfdb29f8 | ||
|
|
26e48f07fd | ||
|
|
8bb5fb9811 | ||
|
|
d41667b599 | ||
|
|
85152cbcd7 | ||
|
|
b80863111f | ||
|
|
6cd88fa51d | ||
|
|
3619e8f47b | ||
|
|
5b41dd24d4 | ||
|
|
91dd2f233a | ||
|
|
7e651f9abc | ||
|
|
e44f666c5c | ||
|
|
c254b52b51 | ||
|
|
37c3a4438f | ||
|
|
302d7511dc | ||
|
|
68d57ba238 | ||
|
|
cf98675223 | ||
|
|
4cc140e4f2 | ||
|
|
2da3a3f010 | ||
|
|
fa6f7ecab0 | ||
|
|
31ab444300 | ||
|
|
85453f5a3a | ||
|
|
6d92539524 | ||
|
|
a605ae6043 | ||
|
|
6aaa6bf042 | ||
|
|
aa578194c7 | ||
|
|
220600070c | ||
|
|
32cdfbbfb0 | ||
|
|
33b83bf242 | ||
|
|
2e1b433365 | ||
|
|
2771a842fe | ||
|
|
4af3d16e61 | ||
|
|
eb47fb051b | ||
|
|
0f9655611b | ||
|
|
0c72ccac12 | ||
|
|
09f7fcd2b4 | ||
|
|
b9250df347 | ||
|
|
ca897db0d2 | ||
|
|
af75d4139c | ||
|
|
d2e35a888d | ||
|
|
fb56c3744b | ||
|
|
26942cfd1f | ||
|
|
1601fc6d81 | ||
|
|
f543a9ff80 | ||
|
|
5299a2a687 | ||
|
|
fcc627db6f | ||
|
|
1035019fc2 | ||
|
|
9d311a7261 | ||
|
|
a973c5fb89 | ||
|
|
be081ccf7a | ||
|
|
c25db02acf | ||
|
|
01f98235c6 | ||
|
|
00f3b87215 | ||
|
|
849958eeec | ||
|
|
9655153e01 | ||
|
|
74d5355e02 | ||
|
|
bb137cc799 | ||
|
|
6aee3d8088 | ||
|
|
51cedcb644 | ||
|
|
270c754c00 | ||
|
|
8f68aca24c | ||
|
|
93710c1e78 | ||
|
|
ac2a3fd38e | ||
|
|
750f1cd63d | ||
|
|
4413528d0e | ||
|
|
e8b992c289 | ||
|
|
938ff38aeb | ||
|
|
77cb534e16 | ||
|
|
58513b63a3 | ||
|
|
712e7ff104 | ||
|
|
c68d283766 | ||
|
|
4d198ff5f1 | ||
|
|
10598d430a | ||
|
|
3fbcce3b04 | ||
|
|
994dac3af4 | ||
|
|
d76e7229fc | ||
|
|
26f072fac7 | ||
|
|
64f96f561b | ||
|
|
23c61b8099 | ||
|
|
3b06b9474c | ||
|
|
3d3410b4fd | ||
|
|
d7f8eec59e | ||
|
|
f98879a1e5 | ||
|
|
ef40e9db5f | ||
|
|
eb799879ff | ||
|
|
13fddc8e7f | ||
|
|
fa3d7f7f4a | ||
|
|
6845ee1664 | ||
|
|
c8b98681ef | ||
|
|
ae4542ce68 | ||
|
|
0140ff5f6e | ||
|
|
a22a47c16a | ||
|
|
6bb7b2ca5d | ||
|
|
1ec7df9a7e | ||
|
|
83925832be | ||
|
|
4dadf98909 | ||
|
|
375c07e442 | ||
|
|
9374541993 | ||
|
|
372224469d | ||
|
|
60e87e8a22 | ||
|
|
353e497642 | ||
|
|
0ee72a9ef8 | ||
|
|
d9873b4261 | ||
|
|
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 |
@@ -15,6 +15,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error'
|
||||
'simple-import-sort/exports': 'error',
|
||||
'react/no-is-mounted': 'off'
|
||||
}
|
||||
}
|
||||
|
||||
3
.github/workflows/release.yml
vendored
@@ -52,8 +52,6 @@ jobs:
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: yarn build:win
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Replace spaces in filenames
|
||||
@@ -73,5 +71,6 @@ jobs:
|
||||
dist/*.rpm
|
||||
dist/*.tar.gz
|
||||
dist/latest*.yml
|
||||
dist/*.blockmap
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
10
.gitignore
vendored
@@ -19,12 +19,6 @@ lerna-debug.log*
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# NPM
|
||||
npm/*/*
|
||||
!npm/*/dist
|
||||
!npm/*/package.json
|
||||
!npm/*/*.js
|
||||
|
||||
# Yarn
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
@@ -41,7 +35,11 @@ Thumbs.db
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
build/icons
|
||||
|
||||
# ENV
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Local
|
||||
local
|
||||
|
||||
3
.vscode/settings.json
vendored
@@ -29,5 +29,6 @@
|
||||
},
|
||||
"[markdown]": {
|
||||
"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
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
32
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,32 @@
|
||||
This software is licensed under the **Apache License 2.0**. In addition to the terms of the Apache License 2.0, the following additional terms apply to the use of Cherry Studio:
|
||||
|
||||
**I. Commercial Use License**
|
||||
|
||||
1. **Free Commercial Use**: Users can use the software for commercial purposes without modifying the code.
|
||||
2. **Commercial License Required**: A commercial license is required if any of the following conditions are met:
|
||||
1. You modify, develop, or alter the software, including but not limited to changes to the application name, logo, code, or functionality.
|
||||
2. You provide multi-tenant services to enterprise customers with 10 or more users.
|
||||
3. You pre-install or integrate the software into hardware devices or products and bundle it for sale.
|
||||
4. You are engaging in large-scale procurement for government or educational institutions, especially involving security, data privacy, or other sensitive requirements.
|
||||
|
||||
**II. Contributor Agreement**
|
||||
|
||||
As a contributor to Cherry Studio, you agree to the following:
|
||||
|
||||
1. **License Adjustment**: The producer reserves the right to adjust the open-source license as needed, making it stricter or more lenient.
|
||||
2. **Commercial Use**: Any code you contribute may be used for commercial purposes, including but not limited to cloud business operations.
|
||||
|
||||
**III. Other Terms**
|
||||
|
||||
1. The interpretation of these terms is subject to the discretion of Cherry Studio developers.
|
||||
2. These terms may be updated, and users will be notified through the software when changes occur.
|
||||
|
||||
For any questions or to request a commercial license, please contact the Cherry Studio development team.
|
||||
|
||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
---
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
72
CONTRIBUTING.md
Normal file
@@ -0,0 +1,72 @@
|
||||
## Cherry Studio目录结构和功能
|
||||
|
||||
### 1. `/src`: 主要源代码目录
|
||||
- ** `/main`**: Electron主进程相关代码
|
||||
- 负责应用的生命周期管理、窗口创建、IPC通信等
|
||||
- ** `/renderer`**: Electron渲染进程相关代码
|
||||
- 包含用户界面的实现,使用TypeScript和SCSS
|
||||
- ** `/preload`**: 预加载脚本
|
||||
- 用于在渲染进程中安全地暴露主进程功能
|
||||
- ** `/components`**: React组件
|
||||
- 可复用的UI组件,如对话框、输入框等
|
||||
- ** `/pages`**: 应用的主要页面
|
||||
- 如聊天界面、设置页面等
|
||||
- ** `/store`**: 状态管理
|
||||
- 可能使用Redux或MobX来管理应用状态
|
||||
- ** `/utils`**: 工具函数
|
||||
- 包含各种辅助函数和工具类
|
||||
- ** `/styles`**: 全局样式文件
|
||||
- 包含SCSS文件,定义全局样式和主题
|
||||
|
||||
### 2. `/public`: 静态资源目录
|
||||
- 包含图标、字体等静态文件
|
||||
|
||||
### 3. `/electron`: Electron相关配置
|
||||
- 包含Electron的构建和打包配置
|
||||
|
||||
### 4. `/scripts`: 构建和开发脚本
|
||||
- 包含npm脚本,用于开发、构建和部署
|
||||
|
||||
### 5. `/types`: TypeScript类型定义
|
||||
- 包含自定义的类型定义文件
|
||||
|
||||
### 6. `/tests`: 测试文件目录
|
||||
- 包含单元测试和集成测试
|
||||
|
||||
### 7. `/docs`: 文档目录
|
||||
- 包含项目文档、API文档等
|
||||
|
||||
### 8. `/config`: 配置文件目录
|
||||
- 包含各种配置文件,如webpack配置、环境变量等
|
||||
|
||||
### 9. `/migrations`: 数据库迁移文件
|
||||
- 由于使用了Sequelize,这里可能包含数据库结构的变更记录
|
||||
|
||||
### 10. `/models`: 数据模型
|
||||
- 定义Sequelize的数据模型,对应数据库表结构
|
||||
|
||||
## 主要功能实现
|
||||
|
||||
### 1. LLM提供商集成
|
||||
- 可能在`/src/utils`或`/src/services`中实现与不同LLM API的集成
|
||||
|
||||
### 2. 多助手和多主题支持
|
||||
- 在`/src/store`中管理助手和主题的状态
|
||||
- 在`/src/components`中实现相关的UI组件
|
||||
|
||||
### 3. 多模型对话
|
||||
- 在`/src/pages`的聊天界面中实现
|
||||
- 可能使用`/src/store`来管理对话状态
|
||||
|
||||
### 4. 拖放排序
|
||||
- 在`/src/components`中实现相关的可拖拽组件
|
||||
|
||||
### 5. 代码高亮
|
||||
- 可能使用第三方库,如Prism.js,集成在`/src/components`中
|
||||
|
||||
### 6. Mermaid图表支持
|
||||
- 在`/src/components`中集成Mermaid库
|
||||
|
||||
### 7. 数据持久化
|
||||
- 使用Sequelize在`/models`中定义数据模型
|
||||
- 在`/migrations`中管理数据库结构变更
|
||||
136
LICENSE
@@ -1,101 +1,79 @@
|
||||
### Cherry Studio 商业许可协议
|
||||
## Cherry Studio 用户协议
|
||||
|
||||
欢迎使用 Cherry Studio 桌面 AI 客户端工具。请仔细阅读以下协议条款,继续使用本软件即表示您同意本协议内容。
|
||||
|
||||
**许可协议**
|
||||
|
||||
本软件采用 Apache License 2.0 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry Studio 时还应遵守以下附加条款:
|
||||
|
||||
**一. 商用许可**
|
||||
|
||||
1. **免费商用**:用户在不修改代码的情况下,可以免费用于商业目的。
|
||||
2. **商业授权**:如果您满足以下任意条件之一,需取得商业授权:
|
||||
1. 对本软件进行二次修改、开发(包括但不限于修改应用名称、logo、代码以及功能)。
|
||||
2. 为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。
|
||||
3. 预装或集成到硬件设备或产品中进行捆绑销售。
|
||||
4. 政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
|
||||
|
||||
**二. 贡献者协议**
|
||||
|
||||
作为 Cherry Studio 的贡献者,您应当同意以下条款:
|
||||
|
||||
1. **许可调整**:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。
|
||||
2. **商业用途**:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。
|
||||
|
||||
**三. 其他条款**
|
||||
|
||||
1. 本协议条款的解释权归 Cherry Studio 开发者所有。
|
||||
2. 本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。
|
||||
|
||||
如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。
|
||||
|
||||
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问 http://www.apache.org/licenses/LICENSE-2.0。
|
||||
|
||||
---
|
||||
|
||||
#### 中文版
|
||||
根据 Apache 许可证 2.0 版(“许可证”)进行许可;除非符合许可证,否则您不得使用此文件。您可以在以下网址获取许可证副本:
|
||||
|
||||
**Cherry Studio 商业许可协议**
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
本协议(以下简称“协议”)由以下双方签订:
|
||||
除非适用法律要求或书面同意,软件根据许可证分发的内容以“原样”分发,不附带任何明示或暗示的保证或条件。请参阅特定语言管理权限的许可证和许可证下的限制。
|
||||
|
||||
- 许可方:王谦(kangfenmao@qq.com)
|
||||
- 被许可方:[被许可方名称]
|
||||
## Cherry Studio User Agreement
|
||||
|
||||
**1. 定义**
|
||||
Welcome to Cherry Studio, a desktop AI client tool. Please read the following agreement carefully. By continuing to use this software, you agree to the terms outlined below.
|
||||
|
||||
- “软件”指 Cherry Studio 软件,网址为 https://cherry-ai.com。
|
||||
- “商业用途”指任何以盈利为目的的使用。
|
||||
**License Agreement**
|
||||
|
||||
**2. 许可**
|
||||
This software is licensed under the **Apache License 2.0**. In addition to the terms of the Apache License 2.0, the following additional terms apply to the use of Cherry Studio:
|
||||
|
||||
- 未经许可方明确书面许可,被许可方不得将软件用于商业用途。
|
||||
- 未经许可方事先书面同意,被许可方不得将软件全部或部分用于商业用途分发。
|
||||
- 未经许可方明确授权,被许可方不得再许可、租赁、销售、出租或以其他方式将软件转让给任何第三方用于商业用途。
|
||||
**I. Commercial Use License**
|
||||
|
||||
**3. 责任限制**
|
||||
1. **Free Commercial Use**: Users can use the software for commercial purposes without modifying the code.
|
||||
2. **Commercial License Required**: A commercial license is required if any of the following conditions are met:
|
||||
1. You modify, develop, or alter the software, including but not limited to changes to the application name, logo, code, or functionality.
|
||||
2. You provide multi-tenant services to enterprise customers with 10 or more users.
|
||||
3. You pre-install or integrate the software into hardware devices or products and bundle it for sale.
|
||||
4. You are engaging in large-scale procurement for government or educational institutions, especially involving security, data privacy, or other sensitive requirements.
|
||||
|
||||
开发者不对因使用本软件而产生的任何直接或间接损失承担责任。用户应自行承担使用本软件的风险。
|
||||
**II. Contributor Agreement**
|
||||
|
||||
**4. 许可协议生效日期**
|
||||
As a contributor to Cherry Studio, you agree to the following:
|
||||
|
||||
本许可协议自用户首次下载或使用本软件之日起生效。
|
||||
1. **License Adjustment**: The producer reserves the right to adjust the open-source license as needed, making it stricter or more lenient.
|
||||
2. **Commercial Use**: Any code you contribute may be used for commercial purposes, including but not limited to cloud business operations.
|
||||
|
||||
**5. 许可终止**
|
||||
**III. Other Terms**
|
||||
|
||||
如发现用户违反上述条款,开发者有权随时终止本许可,并要求用户停止使用本软件及删除所有相关副本。
|
||||
1. The interpretation of these terms is subject to the discretion of Cherry Studio developers.
|
||||
2. These terms may be updated, and users will be notified through the software when changes occur.
|
||||
|
||||
**6. 其他**
|
||||
For any questions or to request a commercial license, please contact the Cherry Studio development team.
|
||||
|
||||
本协议的解释、效力及争议的解决,均适用中华人民共和国法律。
|
||||
|
||||
**7. 联系信息**
|
||||
|
||||
- 许可方联系方式:
|
||||
- 手机号:18539907620
|
||||
- 邮箱:kangfenmao@qq.com
|
||||
|
||||
**许可方(签字):**
|
||||
**日期:**
|
||||
|
||||
**被许可方(签字):**
|
||||
**日期:**
|
||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
---
|
||||
|
||||
#### English Version
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
**Cherry Studio Commercial License Agreement**
|
||||
|
||||
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:**
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
91
README.ja.md
Normal file
@@ -0,0 +1,91 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/releases">
|
||||
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||
</a>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="./README.md">English</a> | <a href="./README.zh.md">中文</a> | 日本語
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。
|
||||
|
||||
👏 [Telegramグループ](https://t.me/CherryStudioAI)に参加しましょう
|
||||
|
||||
# 🌠 スクリーンショット
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 特徴
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
1. 複数のLLMプロバイダーをサポート。
|
||||
2. 複数のアシスタントを作成可能。
|
||||
3. 複数のトピックを作成可能。
|
||||
4. 同じ会話で複数のモデルを使用して質問に回答可能。
|
||||
5. ドラッグアンドドロップでの並べ替えをサポート。
|
||||
6. コードハイライト。
|
||||
7. Mermaidチャート
|
||||
|
||||
# 🖥️ 開発
|
||||
|
||||
## IDEの設定
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
|
||||
## プロジェクトの設定
|
||||
|
||||
### インストール
|
||||
|
||||
```bash
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### 開発
|
||||
|
||||
```bash
|
||||
$ yarn dev
|
||||
```
|
||||
|
||||
### ビルド
|
||||
|
||||
```bash
|
||||
# Windowsの場合
|
||||
$ yarn build:win
|
||||
|
||||
# macOSの場合
|
||||
$ yarn build:mac
|
||||
|
||||
# Linuxの場合
|
||||
$ yarn build:linux
|
||||
```
|
||||
|
||||
# ⭐️ スター履歴
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
# 🚀 コントリビューター
|
||||
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
||||
</a>
|
||||
|
||||
# コミュニティ
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI)
|
||||
|
||||
# スポンサー
|
||||
|
||||
[Buy Me a Coffee](docs/sponsor.md)
|
||||
|
||||
# 📃 ライセンス
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
43
README.md
@@ -1,17 +1,32 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/releases">
|
||||
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||
</a>
|
||||
</div>
|
||||
<div align="center">
|
||||
English | <a href="./README.zh.md">中文</a> | <a href="./README.ja.md">日本語</a>
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
||||
|
||||
👏 Join [Telegram Group](https://t.me/CherryStudioAI)
|
||||
|
||||
# 🌠 Screenshot
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 Features
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
1. Support for Multiple LLM Providers.
|
||||
2. Allows creation of multiple Assistants.
|
||||
3. Enables creation of multiple topics.
|
||||
@@ -22,9 +37,9 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
||||
|
||||
# 🖥️ Develop
|
||||
|
||||
## Recommended IDE Setup
|
||||
## IDE Setup
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
[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)
|
||||
|
||||
## Project Setup
|
||||
|
||||
@@ -57,6 +72,20 @@ $ yarn build:linux
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
# 🚀 Contributors
|
||||
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
||||
</a>
|
||||
|
||||
# Community
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI)
|
||||
|
||||
# Sponsor
|
||||
|
||||
[Buy Me a Coffee](docs/sponsor.md)
|
||||
|
||||
# 📃 License
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
|
||||
110
README.zh.md
Normal file
@@ -0,0 +1,110 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/releases">
|
||||
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||
</a>
|
||||
</div>
|
||||
<div align="center">
|
||||
中文 / <a href="https://github.com/kangfenmao/cherry-studio">English</a> / <a href="./README.ja.md">日本語</a>
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
Cherry Studio 是一款跨平台桌面客户端,支持多个大语言模型(LLM)服务商,兼容 Windows、Mac 和 Linux 系统,并拥丰富的个性化选项与领先的功能设计。
|
||||
|
||||
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)
|
||||
|
||||
# 🌠 界面
|
||||
|
||||
<img width="1582" alt="Xnip2024-09-23_15-01-53" src="https://github.com/user-attachments/assets/554aa31b-87b6-49fe-877d-af313e1608b0">
|
||||
<img width="1582" alt="Xnip2024-09-23_15-02-27" src="https://github.com/user-attachments/assets/f43fb4c8-194a-4f46-8575-6db2bd136cb9">
|
||||
<img width="1582" alt="Xnip2024-09-23_16-12-19" src="https://github.com/user-attachments/assets/82ce3cc1-5a0b-49aa-9fe4-0376d34be1f8">
|
||||
<img width="1582" alt="Xnip2024-09-23_16-11-44" src="https://github.com/user-attachments/assets/55e420c8-fc0f-40a0-868e-d75bebeb5af3">
|
||||
<img width="1582" alt="Xnip2024-09-23_16-11-50" src="https://github.com/user-attachments/assets/7413384e-a7c7-4525-96ea-ccd395d7e51a">
|
||||
<img width="1582" alt="Xnip2024-09-23_16-12-59" src="https://github.com/user-attachments/assets/894b5e97-569f-4471-813c-c48d19455215">
|
||||
|
||||
# 🌟 特性
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
## 😌 轻松上手
|
||||
|
||||
🍏Windows,Mac,Linux跨平台支持
|
||||
|
||||
📦开箱即用,无需 Python 与 Docker
|
||||
|
||||
🤝简洁、友好的界面与交互设计
|
||||
|
||||
## 🛠️多样化的 LLM 服务模式支持
|
||||
|
||||
☁️ 全面覆盖 LLM 云服务,支持自定义 api key 与模型管理:OpenAI,Gemini,Anthropic,硅基流动...
|
||||
|
||||
🔗汇聚流行的 AI Web 服务,并计划通过功能增强提升体验:Claude,Peplexity,Poe,腾讯元宝,知乎直答...
|
||||
|
||||
💻支持 Ollama 运行本地模型
|
||||
|
||||
## 📲个性化的功能体验
|
||||
|
||||
📄完整的 Markdown 与 Mermaid 渲染支持
|
||||
|
||||
🤖使用与创建智能体提升工作效率
|
||||
|
||||
🔤持续迭代的翻译功能
|
||||
|
||||
🤲生成结果支持 Markdown 与图片分享
|
||||
|
||||
📎文件与图片上传,RAG 与多模态对话
|
||||
|
||||
🎨透明窗口与明暗主题支持
|
||||
|
||||
# 🖥️ 开发指南
|
||||
|
||||
## 开发环境
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
|
||||
## 项目设置
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### 启动开发环境
|
||||
|
||||
```bash
|
||||
$ yarn dev
|
||||
```
|
||||
|
||||
### 构建版本
|
||||
|
||||
```bash
|
||||
# For windows
|
||||
$ yarn build:win
|
||||
|
||||
# For macOS
|
||||
$ yarn build:mac
|
||||
|
||||
# For Linux
|
||||
$ yarn build:linux
|
||||
```
|
||||
|
||||
# ⭐️ Star 记录
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
# 社区
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI)
|
||||
|
||||
# 赞助
|
||||
|
||||
[微信赞赏码](docs/sponsor.md)
|
||||
|
||||
# 📃 许可证
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 353 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 489 KiB After Width: | Height: | Size: 210 KiB |
BIN
build/logo.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
95
docs/dev/faq.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# FAQ 文档
|
||||
本文档适用于:产品手册、官网页面、课程测验、现场 Q&A。
|
||||
|
||||
## 问题1:Cherry Studio 支持哪些操作系统?
|
||||
- **答案**:Cherry Studio 支持 Windows、Mac 和 Linux 操作系统。
|
||||
|
||||
## 问题2:Cherry Studio 的主要功能有哪些?
|
||||
- **答案**:Cherry Studio 的主要功能包括:
|
||||
1. 支持多个 LLM 提供商
|
||||
2. 允许创建多个助手
|
||||
3. 支持创建多个主题
|
||||
4. 允许在同一对话中使用多个模型来回答问题
|
||||
5. 支持拖放排序
|
||||
6. 代码高亮
|
||||
7. Mermaid 图表支持
|
||||
|
||||
## 问题3:Cherry Studio 的主要目录结构是怎样的?
|
||||
- **答案**:Cherry Studio 的主要目录结构如下:
|
||||
- `/src`: 主要源代码目录
|
||||
- `/build`: 构建相关文件
|
||||
- `/docs`: 文档目录
|
||||
- `/resources`: 资源文件目录
|
||||
- `/scripts`: 脚本文件目录
|
||||
|
||||
## 问题4:如何在 Windows 环境下 fork Cherry Studio 并修改部分功能?
|
||||
- **答案**:在 Windows 环境下 fork Cherry Studio 并修改部分功能的步骤如下:
|
||||
1. 在 GitHub 上 fork Cherry Studio 仓库
|
||||
2. 克隆 fork 的仓库到本地:`git clone https://github.com/your-username/cherry-studio.git`
|
||||
3. 进入项目目录:`cd cherry-studio`
|
||||
4. 安装依赖:`yarn install`
|
||||
5. 修改所需的功能代码
|
||||
6. 测试修改:`yarn dev`
|
||||
7. 提交修改:`git add .` 和 `git commit -m "描述你的修改"`
|
||||
8. 推送到你的 fork 仓库:`git push origin main`
|
||||
|
||||
## 问题5:Cherry Studio 使用了哪些主要技术栈?
|
||||
- **答案**:Cherry Studio 主要使用了以下技术栈:
|
||||
- TypeScript
|
||||
- SCSS
|
||||
- Electron
|
||||
- Vite
|
||||
- Sequelize
|
||||
|
||||
## 问题6:如何贡献代码到 Cherry Studio 项目?
|
||||
- **答案**:贡献代码到 Cherry Studio 项目的步骤如下:
|
||||
1. Fork 项目仓库
|
||||
2. 创建你的特性分支:`git checkout -b feature/AmazingFeature`
|
||||
3. 提交你的修改:`git commit -m 'Add some AmazingFeature'`
|
||||
4. 推送到分支:`git push origin feature/AmazingFeature`
|
||||
5. 打开一个 Pull Request
|
||||
|
||||
## 问题7:Cherry Studio 的 `/src` 目录主要包含哪些内容?
|
||||
- **答案**:Cherry Studio 的 `/src` 目录主要包含以下内容:
|
||||
- 主进程代码(Electron 主进程)
|
||||
- 渲染进程代码(用户界面)
|
||||
- 组件
|
||||
- 工具函数
|
||||
- 状态管理
|
||||
- 样式文件
|
||||
|
||||
## 问题8:如何在 Cherry Studio 中添加新的 LLM 提供商?
|
||||
- **答案**:要在 Cherry Studio 中添加新的 LLM 提供商,你需要:
|
||||
1. 在 `/src/services` 或类似目录下创建新的服务文件
|
||||
2. 实现与新 LLM 提供商 API 的集成
|
||||
3. 在用户界面中添加新提供商的选项
|
||||
4. 更新配置和状态管理以支持新提供商
|
||||
|
||||
## 问题9:Cherry Studio 的构建过程是怎样的?
|
||||
- **答案**:Cherry Studio 的构建过程主要包括:
|
||||
1. 使用 Vite 构建前端资源
|
||||
2. 使用 Electron Builder 打包桌面应用
|
||||
3. 根据不同平台(Windows、Mac、Linux)生成相应的安装包
|
||||
|
||||
## 问题10:如何在 Cherry Studio 中实现新的 UI 主题?
|
||||
- **答案**:在 Cherry Studio 中实现新的 UI 主题的步骤:
|
||||
1. 在 `/src/styles` 目录下创建新的主题 SCSS 文件
|
||||
2. 定义新主题的颜色变量和样式
|
||||
3. 在主样式文件中导入新主题
|
||||
4. 更新主题切换逻辑以包含新主题
|
||||
5. 在用户界面中添加新主题的选项
|
||||
|
||||
## 问题11:Cherry Studio 如何处理多语言支持?
|
||||
- **答案**:Cherry Studio 可能通过以下方式处理多语言支持:
|
||||
1. 使用 i18n 库进行国际化
|
||||
2. 在 `/src/locales` 或类似目录下存储不同语言的翻译文件
|
||||
3. 实现语言切换功能
|
||||
4. 在组件中使用翻译函数或组件来显示多语言文本
|
||||
|
||||
## 问题12:如何为 Cherry Studio 编写单元测试?
|
||||
- **答案**:为 Cherry Studio 编写单元测试的步骤:
|
||||
1. 在 `/tests` 目录下创建测试文件
|
||||
2. 使用测试框架(如 Jest)编写测试用例
|
||||
3. 模拟 Electron 环境和其他依赖
|
||||
4. 运行测试命令:`yarn test`
|
||||
5. 确保测试覆盖主要功能和组件
|
||||
72
docs/dev/structure.md
Normal file
@@ -0,0 +1,72 @@
|
||||
## Cherry Studio目录结构和功能
|
||||
|
||||
### 1. `/src`: 主要源代码目录
|
||||
- ** `/main`**: Electron主进程相关代码
|
||||
- 负责应用的生命周期管理、窗口创建、IPC通信等
|
||||
- ** `/renderer`**: Electron渲染进程相关代码
|
||||
- 包含用户界面的实现,使用TypeScript和SCSS
|
||||
- ** `/preload`**: 预加载脚本
|
||||
- 用于在渲染进程中安全地暴露主进程功能
|
||||
- ** `/components`**: React组件
|
||||
- 可复用的UI组件,如对话框、输入框等
|
||||
- ** `/pages`**: 应用的主要页面
|
||||
- 如聊天界面、设置页面等
|
||||
- ** `/store`**: 状态管理
|
||||
- 可能使用Redux或MobX来管理应用状态
|
||||
- ** `/utils`**: 工具函数
|
||||
- 包含各种辅助函数和工具类
|
||||
- ** `/styles`**: 全局样式文件
|
||||
- 包含SCSS文件,定义全局样式和主题
|
||||
|
||||
### 2. `/public`: 静态资源目录
|
||||
- 包含图标、字体等静态文件
|
||||
|
||||
### 3. `/electron`: Electron相关配置
|
||||
- 包含Electron的构建和打包配置
|
||||
|
||||
### 4. `/scripts`: 构建和开发脚本
|
||||
- 包含npm脚本,用于开发、构建和部署
|
||||
|
||||
### 5. `/types`: TypeScript类型定义
|
||||
- 包含自定义的类型定义文件
|
||||
|
||||
### 6. `/tests`: 测试文件目录
|
||||
- 包含单元测试和集成测试
|
||||
|
||||
### 7. `/docs`: 文档目录
|
||||
- 包含项目文档、API文档等
|
||||
|
||||
### 8. `/config`: 配置文件目录
|
||||
- 包含各种配置文件,如webpack配置、环境变量等
|
||||
|
||||
### 9. `/migrations`: 数据库迁移文件
|
||||
- 由于使用了Sequelize,这里可能包含数据库结构的变更记录
|
||||
|
||||
### 10. `/models`: 数据模型
|
||||
- 定义Sequelize的数据模型,对应数据库表结构
|
||||
|
||||
## 主要功能实现
|
||||
|
||||
### 1. LLM提供商集成
|
||||
- 可能在`/src/utils`或`/src/services`中实现与不同LLM API的集成
|
||||
|
||||
### 2. 多助手和多主题支持
|
||||
- 在`/src/store`中管理助手和主题的状态
|
||||
- 在`/src/components`中实现相关的UI组件
|
||||
|
||||
### 3. 多模型对话
|
||||
- 在`/src/pages`的聊天界面中实现
|
||||
- 可能使用`/src/store`来管理对话状态
|
||||
|
||||
### 4. 拖放排序
|
||||
- 在`/src/components`中实现相关的可拖拽组件
|
||||
|
||||
### 5. 代码高亮
|
||||
- 可能使用第三方库,如Prism.js,集成在`/src/components`中
|
||||
|
||||
### 6. Mermaid图表支持
|
||||
- 在`/src/components`中集成Mermaid库
|
||||
|
||||
### 7. 数据持久化
|
||||
- 使用Sequelize在`/models`中定义数据模型
|
||||
- 在`/migrations`中管理数据库结构变更
|
||||
5
docs/sponsor.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Sponsor
|
||||
|
||||
<div align="center">
|
||||
<img src="https://github.com/user-attachments/assets/4665f07f-5ecc-4bd8-8727-ae00f35d6d98" alt="Buy Me a Coffee" width="280"/>
|
||||
</div>
|
||||
@@ -3,12 +3,14 @@ productName: Cherry Studio
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!{.vscode,.yarn,.github}'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
- '!src'
|
||||
- '!scripts'
|
||||
- '!local'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
@@ -18,6 +20,8 @@ nsis:
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
allowToChangeInstallationDirectory: true
|
||||
oneClick: false
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
@@ -39,13 +43,16 @@ dmg:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- target: AppImage
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
# - snap
|
||||
# - deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${productName}-${version}.${ext}
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: github
|
||||
@@ -56,4 +63,6 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
错误修复,优化体验
|
||||
增加数学公式渲染引擎切换
|
||||
多个系统滚动条样式统一
|
||||
修复添加默认助手会添加两个
|
||||
|
||||
@@ -4,7 +4,13 @@ import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@types': resolve('src/renderer/src/types'),
|
||||
'@main': resolve('src/main')
|
||||
}
|
||||
}
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
@@ -15,7 +21,6 @@ export default defineConfig({
|
||||
'@renderer': resolve('src/renderer/src')
|
||||
}
|
||||
},
|
||||
plugins: [react()],
|
||||
assetsInclude: ['**/*.md']
|
||||
plugins: [react()]
|
||||
}
|
||||
})
|
||||
|
||||
57
package.json
@@ -1,10 +1,20 @@
|
||||
{
|
||||
"name": "cherry-studio",
|
||||
"version": "0.5.1",
|
||||
"name": "CherryStudio",
|
||||
"version": "0.8.3",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "kangfenmao@qq.com",
|
||||
"homepage": "https://github.com/kangfenmao/cherry-studio",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"local",
|
||||
"packages/*"
|
||||
],
|
||||
"nohoist": [
|
||||
"packages/database"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
@@ -20,53 +30,70 @@
|
||||
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
|
||||
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
|
||||
"release": "node scripts/version.js",
|
||||
"publish": "yarn release patch push"
|
||||
"publish": "yarn release patch push",
|
||||
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
||||
"generate:agents": "yarn workspace @cherry-studio/database agents",
|
||||
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@sentry/electron": "^5.2.0",
|
||||
"archiver": "^7.0.1",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1"
|
||||
"fs-extra": "^11.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"officeparser": "^4.1.1",
|
||||
"unzipper": "^0.12.3",
|
||||
"webdav": "4.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24.3",
|
||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@google/generative-ai": "^0.16.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/node": "^18.19.9",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/unzipper": "^0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"ahooks": "^3.8.0",
|
||||
"antd": "^5.18.3",
|
||||
"axios": "^1.7.3",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"electron": "^28.2.0",
|
||||
"electron": "^28.3.3",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"emittery": "^1.0.3",
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.0.0",
|
||||
"gpt-tokens": "^1.3.6",
|
||||
"gpt-tokens": "^1.3.10",
|
||||
"i18next": "^23.11.5",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.4",
|
||||
"openai": "^4.52.1",
|
||||
"prettier": "^3.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-custom-scrollbars-2": "^4.5.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
@@ -75,13 +102,17 @@
|
||||
"react-router-dom": "6",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-mathjax": "^6.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"sass": "^1.77.2",
|
||||
"styled-components": "^6.1.11",
|
||||
"typescript": "^5.3.3",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^5.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
@@ -90,7 +121,7 @@
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
1
packages/artifacts/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Cherry Studio Artifacts
|
||||
19
packages/artifacts/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@cherry-studio/artifacts",
|
||||
"version": "0.1.0",
|
||||
"description": "Cherry Studio Artifacts",
|
||||
"main": "index.js",
|
||||
"homepage": "https://github.com/kangfenmao/cherry-studio/blob/main/npm/artifacts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"artifacts"
|
||||
],
|
||||
"author": "kangfenmao",
|
||||
"license": "ISC"
|
||||
}
|
||||
108
packages/artifacts/statics/word-explanation-card.css
Normal file
@@ -0,0 +1,108 @@
|
||||
:root {
|
||||
/* 莫兰迪色系:使用柔和、低饱和度的颜色 */
|
||||
--primary-color: #b6b5a7; /* 莫兰迪灰褐色,用于背景文字 */
|
||||
--secondary-color: #9a8f8f; /* 莫兰迪灰棕色,用于标题背景 */
|
||||
--accent-color: #c5b4a0; /* 莫兰迪淡棕色,用于强调元素 */
|
||||
--background-color: #e8e3de; /* 莫兰迪米色,用于页面背景 */
|
||||
--text-color: #5b5b5b; /* 莫兰迪深灰色,用于主要文字 */
|
||||
--light-text-color: #8c8c8c; /* 莫兰迪中灰色,用于次要文字 */
|
||||
--divider-color: #d1cbc3; /* 莫兰迪浅灰色,用于分隔线 */
|
||||
}
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--background-color); /* 使用莫兰迪米色作为页面背景 */
|
||||
font-family: 'Noto Sans SC', sans-serif;
|
||||
color: var(--text-color); /* 使用莫兰迪深灰色作为主要文字颜色 */
|
||||
}
|
||||
.card {
|
||||
width: 300px;
|
||||
height: 500px;
|
||||
background-color: #f2ede9; /* 莫兰迪浅米色,用于卡片背景 */
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header {
|
||||
background-color: var(--secondary-color); /* 使用莫兰迪灰棕色作为标题背景 */
|
||||
color: #f2ede9; /* 浅色文字与深色背景形成对比 */
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
.content {
|
||||
padding: 30px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.word {
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.word-main {
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 36px;
|
||||
color: var(--text-color); /* 使用莫兰迪深灰色作为主要词汇颜色 */
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.word-main::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -5px;
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
background-color: var(--accent-color); /* 使用莫兰迪淡棕色作为下划线 */
|
||||
}
|
||||
.word-sub {
|
||||
font-size: 14px;
|
||||
color: var(--light-text-color); /* 使用莫兰迪中灰色作为次要文字颜色 */
|
||||
margin: 5px 0;
|
||||
}
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color); /* 使用莫兰迪浅灰色作为分隔线 */
|
||||
margin: 20px 0;
|
||||
}
|
||||
.explanation {
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.quote {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
border-left: 3px solid var(--accent-color); /* 使用莫兰迪淡棕色作为引用边框 */
|
||||
}
|
||||
.background-text {
|
||||
position: absolute;
|
||||
font-size: 150px;
|
||||
color: rgba(182, 181, 167, 0.15); /* 使用莫兰迪灰褐色的透明版本作为背景文字 */
|
||||
z-index: 0;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: bold;
|
||||
}
|
||||
3
packages/database/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
data/*
|
||||
!data/.gitkeep
|
||||
|
||||
BIN
packages/database/.yarn/install-state.gz
Normal file
3
packages/database/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Cherry Studio Database
|
||||
|
||||
Cherry Studio 依赖的数据文件由这个数据库来生成,数据库文件请联系开发者获取
|
||||
0
packages/database/data/.gitkeep
Normal file
13
packages/database/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@cherry-studio/database",
|
||||
"packageManager": "yarn@4.3.1",
|
||||
"dependencies": {
|
||||
"csv-parser": "^3.0.0",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"scripts": {
|
||||
"agents": "node src/agents.js",
|
||||
"email": "yarn csv && node src/email.js",
|
||||
"csv": "node src/csv.js"
|
||||
}
|
||||
}
|
||||
45
packages/database/src/agents.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const sqlite3 = require('sqlite3').verbose()
|
||||
const fs = require('fs')
|
||||
|
||||
// 连接到数据库
|
||||
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
|
||||
if (err) {
|
||||
console.error('Error connecting to the database:', err.message)
|
||||
return
|
||||
}
|
||||
console.log('Connected to the database.')
|
||||
})
|
||||
|
||||
// 查询数据并转换为JSON
|
||||
db.all('SELECT * FROM agents', [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error querying the database:', err.message)
|
||||
return
|
||||
}
|
||||
|
||||
// 将 ID 类型转换为字符串
|
||||
for (const row of rows) {
|
||||
row.id = row.id.toString()
|
||||
}
|
||||
|
||||
// 将查询结果转换为JSON字符串
|
||||
const jsonData = JSON.stringify(rows, null, 2)
|
||||
|
||||
// 将JSON数据写入文件
|
||||
fs.writeFile('../../src/renderer/src/config/agents.json', jsonData, (err) => {
|
||||
if (err) {
|
||||
console.error('Error writing to file:', err.message)
|
||||
return
|
||||
}
|
||||
console.log('Data has been written to agents.json')
|
||||
})
|
||||
|
||||
// 关闭数据库连接
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing the database:', err.message)
|
||||
return
|
||||
}
|
||||
console.log('Database connection closed.')
|
||||
})
|
||||
})
|
||||
77
packages/database/src/csv.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const fs = require('fs')
|
||||
const csv = require('csv-parser')
|
||||
const sqlite3 = require('sqlite3').verbose()
|
||||
|
||||
// 连接到 SQLite 数据库
|
||||
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database', err)
|
||||
return
|
||||
}
|
||||
console.log('Connected to the SQLite database.')
|
||||
})
|
||||
|
||||
// 创建一个数组来存储 CSV 数据
|
||||
const results = []
|
||||
|
||||
// 读取 CSV 文件
|
||||
fs.createReadStream('./data/data.csv')
|
||||
.pipe(csv())
|
||||
.on('data', (data) => results.push(data))
|
||||
.on('end', () => {
|
||||
// 准备 SQL 插入语句,使用 INSERT OR IGNORE
|
||||
const stmt = db.prepare('INSERT OR IGNORE INTO emails (email, github, sent) VALUES (?, ?, ?)')
|
||||
|
||||
// 插入每一行数据
|
||||
let inserted = 0
|
||||
let skipped = 0
|
||||
let emptyEmail = 0
|
||||
|
||||
db.serialize(() => {
|
||||
// 开始一个事务以提高性能
|
||||
db.run('BEGIN TRANSACTION')
|
||||
|
||||
results.forEach((row) => {
|
||||
// 检查 email 是否为空
|
||||
if (!row.email || row.email.trim() === '') {
|
||||
emptyEmail++
|
||||
return // 跳过这一行
|
||||
}
|
||||
|
||||
stmt.run(row.email, row['user-href'], 0, function (err) {
|
||||
if (err) {
|
||||
console.error('Error inserting row', err)
|
||||
} else {
|
||||
if (this.changes === 1) {
|
||||
inserted++
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 提交事务
|
||||
db.run('COMMIT', (err) => {
|
||||
if (err) {
|
||||
console.error('Error committing transaction', err)
|
||||
} else {
|
||||
console.log(
|
||||
`Insertion complete. Inserted: ${inserted}, Skipped (duplicate): ${skipped}, Skipped (empty email): ${emptyEmail}`
|
||||
)
|
||||
}
|
||||
|
||||
// 完成插入
|
||||
stmt.finalize()
|
||||
|
||||
// 关闭数据库连接
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database', err)
|
||||
} else {
|
||||
console.log('Database connection closed.')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
36
packages/database/src/email.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const sqlite3 = require('sqlite3').verbose()
|
||||
|
||||
// 连接到数据库
|
||||
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
|
||||
if (err) {
|
||||
console.error('Error connecting to the database:', err.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// 查询数据并转换为JSON
|
||||
db.all('SELECT * FROM emails WHERE sent = 0', [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error querying the database:', err.message)
|
||||
return
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
console.log(row.email)
|
||||
// Update row set sent = 1
|
||||
db.run('UPDATE emails SET sent = 1 WHERE id = ?', [row.id], (err) => {
|
||||
if (err) {
|
||||
console.error('Error updating the database:', err.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing the database:', err.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
1643
packages/database/yarn.lock
Normal file
118
resources/cherry-studio/license.html
Normal file
@@ -0,0 +1,118 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CherryStudio 许可协议-ZH/EN</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 p-8">
|
||||
<div class="container mx-auto bg-white p-6 rounded shadow-lg">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio 许可协议</h1>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">许可协议</h2>
|
||||
<p class="mb-4">
|
||||
本软件采用 <strong>Apache License 2.0</strong> 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry
|
||||
Studio 时还应遵守以下附加条款:
|
||||
</p>
|
||||
<h3 class="text-xl font-semibold mb-2">一. 商用许可</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li><strong>免费商用</strong>:用户在不修改代码的情况下,可以免费用于商业目的。</li>
|
||||
<li>
|
||||
<strong>商业授权</strong>:如果您满足以下任意条件之一,需取得商业授权:
|
||||
<ol class="list-decimal list-inside ml-4">
|
||||
<li>对本软件进行二次修改、开发(包括但不限于修改应用名称、logo、代码以及功能)。</li>
|
||||
<li>为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。</li>
|
||||
<li>预装或集成到硬件设备或产品中进行捆绑销售。</li>
|
||||
<li>政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">二. 贡献者协议</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li><strong>许可调整</strong>:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。</li>
|
||||
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">三. 其他条款</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>本协议条款的解释权归 Cherry Studio 开发者所有。</li>
|
||||
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
|
||||
</ol>
|
||||
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p>
|
||||
<p>
|
||||
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
|
||||
</p>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">License Agreement</h2>
|
||||
<p class="mb-4">
|
||||
This software is licensed under the <strong>Apache License 2.0</strong>. In addition to the terms of the
|
||||
Apache License 2.0, the following additional terms apply to the use of Cherry Studio:
|
||||
</p>
|
||||
<h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>
|
||||
<strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without
|
||||
modifying
|
||||
the code.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Commercial License Required</strong>: A commercial license is required if any of the
|
||||
following
|
||||
conditions are met:
|
||||
<ol class="list-decimal list-inside ml-4">
|
||||
<li>
|
||||
You modify, develop, or alter the software, including but not limited to changes to the
|
||||
application
|
||||
name, logo, code, or functionality.
|
||||
</li>
|
||||
<li>You provide multi-tenant services to enterprise customers with 10 or more users.</li>
|
||||
<li>
|
||||
You pre-install or integrate the software into hardware devices or products and bundle it
|
||||
for sale.
|
||||
</li>
|
||||
<li>
|
||||
You are engaging in large-scale procurement for government or educational institutions,
|
||||
especially
|
||||
involving security, data privacy, or other sensitive requirements.
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>
|
||||
<strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source
|
||||
license as
|
||||
needed, making it stricter or more lenient.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes,
|
||||
including but
|
||||
not limited to cloud business operations.
|
||||
</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">III. Other Terms</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>The interpretation of these terms is subject to the discretion of Cherry Studio developers.</li>
|
||||
<li>These terms may be updated, and users will be notified through the software when changes occur.</li>
|
||||
</ol>
|
||||
<p class="mb-4">
|
||||
For any questions or to request a commercial license, please contact the Cherry Studio development team.
|
||||
</p>
|
||||
<p>
|
||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache
|
||||
License 2.0. Detailed information about the Apache License 2.0 can be found at
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
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>
|
||||
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)
|
||||
}
|
||||
@@ -1,15 +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,
|
||||
height: 40,
|
||||
color: '#00000000',
|
||||
symbolColor: '#ffffff'
|
||||
}
|
||||
|
||||
export const titleBarOverlayLight = {
|
||||
height: 41,
|
||||
height: 40,
|
||||
color: '#00000000',
|
||||
symbolColor: '#000000'
|
||||
}
|
||||
|
||||
91
src/main/constant.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||
export const textExts = [
|
||||
'.txt', // 普通文本文件
|
||||
'.md', // Markdown 文件
|
||||
'.mdx', // Markdown 文件
|
||||
'.html', // HTML 文件
|
||||
'.htm', // HTML 文件的另一种扩展名
|
||||
'.xml', // XML 文件
|
||||
'.json', // JSON 文件
|
||||
'.yaml', // YAML 文件
|
||||
'.yml', // YAML 文件的另一种扩展名
|
||||
'.csv', // 逗号分隔值文件
|
||||
'.tsv', // 制表符分隔值文件
|
||||
'.ini', // 配置文件
|
||||
'.log', // 日志文件
|
||||
'.rtf', // 富文本格式文件
|
||||
'.tex', // LaTeX 文件
|
||||
'.srt', // 字幕文件
|
||||
'.xhtml', // XHTML 文件
|
||||
'.nfo', // 信息文件(主要用于场景发布)
|
||||
'.conf', // 配置文件
|
||||
'.config', // 配置文件
|
||||
'.env', // 环境变量文件
|
||||
'.rst', // reStructuredText 文件
|
||||
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||
'.ts', // TypeScript 文件
|
||||
'.jsp', // JavaServer Pages 文件
|
||||
'.aspx', // ASP.NET 文件
|
||||
'.bat', // Windows 批处理文件
|
||||
'.sh', // Unix/Linux Shell 脚本文件
|
||||
'.py', // Python 脚本文件
|
||||
'.rb', // Ruby 脚本文件
|
||||
'.pl', // Perl 脚本文件
|
||||
'.sql', // SQL 脚本文件
|
||||
'.css', // Cascading Style Sheets 文件
|
||||
'.less', // Less CSS 预处理器文件
|
||||
'.scss', // Sass CSS 预处理器文件
|
||||
'.sass', // Sass 文件
|
||||
'.styl', // Stylus CSS 预处理器文件
|
||||
'.coffee', // CoffeeScript 文件
|
||||
'.ino', // Arduino 代码文件
|
||||
'.asm', // Assembly 语言文件
|
||||
'.go', // Go 语言文件
|
||||
'.scala', // Scala 语言文件
|
||||
'.swift', // Swift 语言文件
|
||||
'.kt', // Kotlin 语言文件
|
||||
'.rs', // Rust 语言文件
|
||||
'.lua', // Lua 语言文件
|
||||
'.groovy', // Groovy 语言文件
|
||||
'.dart', // Dart 语言文件
|
||||
'.hs', // Haskell 语言文件
|
||||
'.clj', // Clojure 语言文件
|
||||
'.cljs', // ClojureScript 语言文件
|
||||
'.elm', // Elm 语言文件
|
||||
'.erl', // Erlang 语言文件
|
||||
'.ex', // Elixir 语言文件
|
||||
'.exs', // Elixir 脚本文件
|
||||
'.pug', // Pug (formerly Jade) 模板文件
|
||||
'.haml', // Haml 模板文件
|
||||
'.slim', // Slim 模板文件
|
||||
'.tpl', // 模板文件(通用)
|
||||
'.ejs', // Embedded JavaScript 模板文件
|
||||
'.hbs', // Handlebars 模板文件
|
||||
'.mustache', // Mustache 模板文件
|
||||
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||
'.twig', // Twig 模板文件
|
||||
'.blade', // Blade 模板文件 (Laravel)
|
||||
'.vue', // Vue.js 单文件组件
|
||||
'.jsx', // React JSX 文件
|
||||
'.tsx', // React TSX 文件
|
||||
'.graphql', // GraphQL 查询语言文件
|
||||
'.gql', // GraphQL 查询语言文件
|
||||
'.proto', // Protocol Buffers 文件
|
||||
'.thrift', // Thrift 文件
|
||||
'.toml', // TOML 配置文件
|
||||
'.edn', // Clojure 数据表示文件
|
||||
'.cake', // CakePHP 配置文件
|
||||
'.ctp', // CakePHP 视图文件
|
||||
'.cfm', // ColdFusion 标记语言文件
|
||||
'.cfc', // ColdFusion 组件文件
|
||||
'.m', // Objective-C 源文件
|
||||
'.mm', // Objective-C++ 源文件
|
||||
'.gradle', // Gradle 构建文件
|
||||
'.groovy', // Gradle 构建文件
|
||||
'.kts', // Kotlin Script 文件
|
||||
'.java' // Java 代码文件
|
||||
]
|
||||
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,24 +0,0 @@
|
||||
import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||
import logger from 'electron-log'
|
||||
import { writeFile } from 'fs'
|
||||
|
||||
export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise<void> {
|
||||
try {
|
||||
const options: SaveDialogOptions = {
|
||||
title: '保存文件',
|
||||
defaultPath: fileName
|
||||
}
|
||||
|
||||
const result: SaveDialogReturnValue = await dialog.showSaveDialog(options)
|
||||
|
||||
if (!result.canceled && result.filePath) {
|
||||
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
|
||||
if (err) {
|
||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +1,19 @@
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import * as Sentry from '@sentry/electron/main'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { saveFile } from './event'
|
||||
import AppUpdater from './updater'
|
||||
|
||||
function createWindow() {
|
||||
// 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
|
||||
// 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.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
import { registerIpc } from './ipc'
|
||||
import { updateUserDataPath } from './utils/upgrade'
|
||||
import { createMainWindow } from './window'
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
await updateUserDataPath()
|
||||
|
||||
// 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
|
||||
// and ignore CommandOrControl + R in production.
|
||||
@@ -97,45 +25,18 @@ app.whenReady().then(() => {
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||
})
|
||||
|
||||
const mainWindow = createWindow()
|
||||
const mainWindow = createMainWindow()
|
||||
|
||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||
|
||||
// IPC
|
||||
ipcMain.handle('get-app-info', () => ({
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged
|
||||
}))
|
||||
|
||||
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('set-theme', (_, theme: 'light' | 'dark') => {
|
||||
appConfig.set('theme', theme)
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
})
|
||||
|
||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||
ipcMain.handle('check-for-update', async () => {
|
||||
autoUpdater.logger?.info('触发检查更新')
|
||||
return {
|
||||
currentVersion: autoUpdater.currentVersion,
|
||||
update: await autoUpdater.checkForUpdates()
|
||||
}
|
||||
})
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
}
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
@@ -149,6 +50,3 @@ app.on('window-all-closed', () => {
|
||||
|
||||
// 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.
|
||||
Sentry.init({
|
||||
dsn: 'https://f0e972deff79c2df3e887e232d8a46a3@o4507610668007424.ingest.us.sentry.io/4507610670563328'
|
||||
})
|
||||
|
||||
78
src/main/ipc.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
import BackupManager from './services/BackupManager'
|
||||
import FileManager from './services/FileManager'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import { createMinappWindow } from './window'
|
||||
|
||||
const fileManager = new FileManager()
|
||||
const backupManager = new BackupManager()
|
||||
|
||||
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('reload', () => mainWindow.reload())
|
||||
|
||||
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
||||
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
||||
ipcMain.handle('backup:backup', backupManager.backup)
|
||||
ipcMain.handle('backup:restore', backupManager.restore)
|
||||
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||
|
||||
ipcMain.handle('file:open', fileManager.open)
|
||||
ipcMain.handle('file:save', fileManager.save)
|
||||
ipcMain.handle('file:select', fileManager.selectFile)
|
||||
ipcMain.handle('file:upload', fileManager.uploadFile)
|
||||
ipcMain.handle('file:clear', fileManager.clear)
|
||||
ipcMain.handle('file:read', fileManager.readFile)
|
||||
ipcMain.handle('file:delete', fileManager.deleteFile)
|
||||
ipcMain.handle('file:get', fileManager.getFile)
|
||||
ipcMain.handle('file:selectFolder', fileManager.selectFolder)
|
||||
ipcMain.handle('file:create', fileManager.createTempFile)
|
||||
ipcMain.handle('file:write', fileManager.writeFile)
|
||||
ipcMain.handle('file:saveImage', fileManager.saveImage)
|
||||
ipcMain.handle('file:base64Image', fileManager.base64Image)
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -17,16 +17,13 @@ export default class AppUpdater {
|
||||
mainWindow.webContents.send('update-error', error)
|
||||
})
|
||||
|
||||
// 检测是否需要更新
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
logger.info('正在检查更新……')
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
||||
mainWindow.webContents.send('update-available', releaseInfo)
|
||||
|
||||
const releaseNotes = releaseInfo.releaseNotes
|
||||
let releaseContent = ''
|
||||
|
||||
if (releaseNotes) {
|
||||
if (typeof releaseNotes === 'string') {
|
||||
releaseContent = <string>releaseNotes
|
||||
@@ -59,7 +56,6 @@ export default class AppUpdater {
|
||||
|
||||
// 检测到不需要更新时
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
logger.info('现在使用的就是最新版本,不用更新')
|
||||
mainWindow.webContents.send('update-not-available')
|
||||
})
|
||||
|
||||
116
src/main/services/BackupManager.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import archiver from 'archiver'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import * as unzipper from 'unzipper'
|
||||
|
||||
import WebDav from './WebDav'
|
||||
|
||||
class BackupManager {
|
||||
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
||||
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
|
||||
|
||||
constructor() {
|
||||
this.backup = this.backup.bind(this)
|
||||
this.restore = this.restore.bind(this)
|
||||
this.backupToWebdav = this.backupToWebdav.bind(this)
|
||||
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
||||
}
|
||||
|
||||
async backup(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
fileName: string,
|
||||
data: string,
|
||||
destinationPath: string = this.backupDir
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
|
||||
// 将 data 写入临时文件
|
||||
const tempDataPath = path.join(this.tempDir, 'data.json')
|
||||
await fs.writeFile(tempDataPath, data)
|
||||
|
||||
// 复制 Data 目录到临时目录
|
||||
const sourcePath = path.join(app.getPath('userData'), 'Data')
|
||||
const tempDataDir = path.join(this.tempDir, 'Data')
|
||||
await fs.copy(sourcePath, tempDataDir)
|
||||
|
||||
// 创建 zip 文件
|
||||
const output = fs.createWriteStream(path.join(destinationPath, fileName))
|
||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
||||
|
||||
archive.pipe(output)
|
||||
archive.directory(this.tempDir, false)
|
||||
await archive.finalize()
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('Backup completed successfully')
|
||||
|
||||
const backupedFilePath = path.join(destinationPath, fileName)
|
||||
|
||||
return backupedFilePath
|
||||
} catch (error) {
|
||||
Logger.error('Backup failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
|
||||
// 解压备份文件到临时目录
|
||||
await fs
|
||||
.createReadStream(backupPath)
|
||||
.pipe(unzipper.Extract({ path: this.tempDir }))
|
||||
.promise()
|
||||
|
||||
// 读取 data.json
|
||||
const dataPath = path.join(this.tempDir, 'data.json')
|
||||
const data = await fs.readFile(dataPath, 'utf-8')
|
||||
|
||||
// 恢复 Data 目录
|
||||
const sourcePath = path.join(this.tempDir, 'Data')
|
||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||
await fs.remove(destPath)
|
||||
await fs.copy(sourcePath, destPath)
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('Restore completed successfully')
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
||||
const filename = 'cherry-studio.backup.zip'
|
||||
const backupedFilePath = await this.backup(_, filename, data)
|
||||
const webdavClient = new WebDav(webdavConfig)
|
||||
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
||||
overwrite: true
|
||||
})
|
||||
}
|
||||
|
||||
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
||||
const filename = 'cherry-studio.backup.zip'
|
||||
const webdavClient = new WebDav(webdavConfig)
|
||||
const retrievedFile = await webdavClient.getFileContents(filename)
|
||||
const backupedFilePath = path.join(this.backupDir, filename)
|
||||
|
||||
if (!fs.existsSync(this.backupDir)) {
|
||||
fs.mkdirSync(this.backupDir, { recursive: true })
|
||||
}
|
||||
|
||||
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
||||
|
||||
return await this.restore(_, backupedFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
export default BackupManager
|
||||
320
src/main/services/FileManager.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { documentExts } from '@main/constant'
|
||||
import { getFileType } from '@main/utils/file'
|
||||
import { FileType } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import {
|
||||
app,
|
||||
dialog,
|
||||
OpenDialogOptions,
|
||||
OpenDialogReturnValue,
|
||||
SaveDialogOptions,
|
||||
SaveDialogReturnValue
|
||||
} from 'electron'
|
||||
import logger from 'electron-log'
|
||||
import * as fs from 'fs'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { readFile } from 'fs/promises'
|
||||
import officeParser from 'officeparser'
|
||||
import * as path from 'path'
|
||||
import { chdir } from 'process'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
class FileManager {
|
||||
private storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
|
||||
private tempDir = path.join(app.getPath('temp'), 'CherryStudio')
|
||||
|
||||
constructor() {
|
||||
this.initStorageDir()
|
||||
}
|
||||
|
||||
private initStorageDir = (): void => {
|
||||
if (!fs.existsSync(this.storageDir)) {
|
||||
fs.mkdirSync(this.storageDir, { recursive: true })
|
||||
}
|
||||
if (!fs.existsSync(this.tempDir)) {
|
||||
fs.mkdirSync(this.tempDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
private getFileHash = async (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)
|
||||
})
|
||||
}
|
||||
|
||||
findDuplicateFile = async (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
|
||||
}
|
||||
|
||||
public selectFile = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
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)
|
||||
}
|
||||
|
||||
public uploadFile = async (_: Electron.IpcMainInvokeEvent, 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
|
||||
}
|
||||
|
||||
public getFile = async (_: Electron.IpcMainInvokeEvent, 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
|
||||
}
|
||||
|
||||
public deleteFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
|
||||
await fs.promises.unlink(path.join(this.storageDir, id))
|
||||
}
|
||||
|
||||
public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<string> => {
|
||||
const filePath = path.join(this.storageDir, id)
|
||||
|
||||
if (documentExts.includes(path.extname(filePath))) {
|
||||
const originalCwd = process.cwd()
|
||||
try {
|
||||
chdir(this.tempDir)
|
||||
const data = await officeParser.parseOfficeAsync(filePath)
|
||||
chdir(originalCwd)
|
||||
return data
|
||||
} catch (error) {
|
||||
chdir(originalCwd)
|
||||
logger.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return fs.readFileSync(filePath, 'utf8')
|
||||
}
|
||||
|
||||
public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise<string> => {
|
||||
if (!fs.existsSync(this.tempDir)) {
|
||||
fs.mkdirSync(this.tempDir, { recursive: true })
|
||||
}
|
||||
const tempFilePath = path.join(this.tempDir, `temp_file_${uuidv4()}_${fileName}`)
|
||||
return tempFilePath
|
||||
}
|
||||
|
||||
public writeFile = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
filePath: string,
|
||||
data: Uint8Array | string
|
||||
): Promise<void> => {
|
||||
await fs.promises.writeFile(filePath, data)
|
||||
}
|
||||
|
||||
public base64Image = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
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}`
|
||||
}
|
||||
}
|
||||
|
||||
public clear = async (): Promise<void> => {
|
||||
await fs.promises.rmdir(this.storageDir, { recursive: true })
|
||||
await this.initStorageDir()
|
||||
}
|
||||
|
||||
public open = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
options: OpenDialogOptions
|
||||
): Promise<{ fileName: string; filePath: 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, filePath, content }
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (err) {
|
||||
logger.error('[IPC - Error]', 'An error occurred opening the file:', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public save = async (
|
||||
_: 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)
|
||||
}
|
||||
}
|
||||
|
||||
public saveImage = async (_: Electron.IpcMainInvokeEvent, name: string, data: string): Promise<void> => {
|
||||
try {
|
||||
const filePath = dialog.showSaveDialogSync({
|
||||
defaultPath: `${name}.png`,
|
||||
filters: [{ name: 'PNG Image', extensions: ['png'] }]
|
||||
})
|
||||
|
||||
if (filePath) {
|
||||
const base64Data = data.replace(/^data:image\/png;base64,/, '')
|
||||
fs.writeFileSync(filePath, base64Data, 'base64')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[IPC - Error]', 'An error occurred saving the image:', error)
|
||||
}
|
||||
}
|
||||
|
||||
public selectFolder = async (_: Electron.IpcMainInvokeEvent, options: OpenDialogOptions): Promise<string | null> => {
|
||||
try {
|
||||
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
|
||||
title: '选择文件夹',
|
||||
properties: ['openDirectory'],
|
||||
...options
|
||||
})
|
||||
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
return result.filePaths[0]
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (err) {
|
||||
logger.error('[IPC - Error]', 'An error occurred selecting the folder:', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FileManager
|
||||
66
src/main/services/WebDav.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
import Stream from 'stream'
|
||||
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
|
||||
|
||||
export default class WebDav {
|
||||
public instance: WebDAVClient | undefined
|
||||
private webdavPath: string
|
||||
|
||||
constructor(params: WebDavConfig) {
|
||||
this.webdavPath = params.webdavPath
|
||||
|
||||
this.instance = createClient(params.webdavHost, {
|
||||
username: params.webdavUser,
|
||||
password: params.webdavPass
|
||||
})
|
||||
|
||||
this.putFileContents = this.putFileContents.bind(this)
|
||||
this.getFileContents = this.getFileContents.bind(this)
|
||||
}
|
||||
|
||||
public putFileContents = async (
|
||||
filename: string,
|
||||
data: string | BufferLike | Stream.Readable,
|
||||
options?: PutFileContentsOptions
|
||||
) => {
|
||||
if (!this.instance) {
|
||||
return new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
if (!(await this.instance.exists(this.webdavPath))) {
|
||||
await this.instance.createDirectory(this.webdavPath, {
|
||||
recursive: true
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error creating directory on WebDAV:', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||
|
||||
try {
|
||||
return await this.instance.putFileContents(remoteFilePath, data, options)
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error putting file contents on WebDAV:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public getFileContents = async (filename: string, options?: GetFileContentsOptions) => {
|
||||
if (!this.instance) {
|
||||
throw new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||
|
||||
try {
|
||||
return await this.instance.getFileContents(remoteFilePath, options)
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error getting file contents on WebDAV:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
13
src/main/utils/file.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@main/constant'
|
||||
|
||||
import { FileTypes } from '../../renderer/src/types'
|
||||
|
||||
export function getFileType(ext: string): FileTypes {
|
||||
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
|
||||
}
|
||||
}
|
||||
127
src/main/window.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
|
||||
export function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = appConfig.get('theme') || 'light'
|
||||
|
||||
// Create the browser window.
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'fullscreen-ui',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
// devTools: !app.isPackaged,
|
||||
}
|
||||
})
|
||||
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({ label: '复制', role: 'copy' }))
|
||||
menu.append(new MenuItem({ label: '粘贴', role: 'paste' }))
|
||||
menu.append(new MenuItem({ label: '剪切', role: 'cut' }))
|
||||
menu.popup()
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
if (details.responseHeaders?.['X-Frame-Options']) {
|
||||
delete details.responseHeaders['X-Frame-Options']
|
||||
}
|
||||
if (details.responseHeaders?.['x-frame-options']) {
|
||||
delete details.responseHeaders['x-frame-options']
|
||||
}
|
||||
if (details.responseHeaders?.['Content-Security-Policy']) {
|
||||
delete details.responseHeaders['Content-Security-Policy']
|
||||
}
|
||||
if (details.responseHeaders?.['content-security-policy']) {
|
||||
delete details.responseHeaders['content-security-policy']
|
||||
}
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders })
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
export function createMinappWindow({
|
||||
url,
|
||||
parent,
|
||||
windowOptions
|
||||
}: {
|
||||
url: string
|
||||
parent?: BrowserWindow
|
||||
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||
}) {
|
||||
const width = windowOptions?.width || 1000
|
||||
const height = windowOptions?.height || 680
|
||||
|
||||
const minappWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
autoHideMenuBar: true,
|
||||
title: 'Cherry Studio',
|
||||
...windowOptions,
|
||||
parent,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/minapp.js'),
|
||||
sandbox: false,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
minappWindow.loadURL(url)
|
||||
|
||||
return minappWindow
|
||||
}
|
||||
31
src/preload/index.d.ts
vendored
@@ -1,4 +1,8 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import { FileType } from '@renderer/types'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
import type { OpenDialogOptions } from 'electron'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -7,12 +11,37 @@ declare global {
|
||||
getAppInfo: () => Promise<{
|
||||
version: string
|
||||
isPackaged: boolean
|
||||
appPath: string
|
||||
}>
|
||||
checkForUpdate: () => void
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
saveFile: (path: string, content: string) => void
|
||||
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>
|
||||
backup: {
|
||||
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
|
||||
restore: (backupPath: string) => Promise<string>
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
||||
}
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||
upload: (file: FileType) => Promise<FileType>
|
||||
delete: (fileId: string) => Promise<void>
|
||||
read: (fileId: string) => Promise<string>
|
||||
clear: () => Promise<void>
|
||||
get: (filePath: string) => Promise<FileType | null>
|
||||
selectFolder: () => Promise<string | null>
|
||||
create: (fileName: string) => Promise<string>
|
||||
write: (filePath: string, data: Uint8Array | string) => Promise<void>
|
||||
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
|
||||
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
|
||||
saveImage: (name: string, data: string) => void
|
||||
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
@@ -7,8 +8,35 @@ const api = {
|
||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme)
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
|
||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||
reload: () => ipcRenderer.invoke('reload'),
|
||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text),
|
||||
backup: {
|
||||
backup: (fileName: string, data: string, destinationPath?: string) =>
|
||||
ipcRenderer.invoke('backup:backup', fileName, data, destinationPath),
|
||||
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
|
||||
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig)
|
||||
},
|
||||
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),
|
||||
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),
|
||||
open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options),
|
||||
save: (path: string, content: string, options?: { compress: boolean }) =>
|
||||
ipcRenderer.invoke('file:save', path, content, options),
|
||||
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
|
||||
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId)
|
||||
}
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Cherry Studio</title>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:" />
|
||||
</head>
|
||||
<body>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#spinner img {
|
||||
width: 100px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="spinner">
|
||||
<img src="/src/assets/images/logo.png" />
|
||||
</div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,3 +1,5 @@
|
||||
import '@renderer/databases'
|
||||
|
||||
import store, { persistor } from '@renderer/store'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
@@ -5,12 +7,15 @@ import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HistoryPage from './pages/history/HistoryPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
import AntdProvider from './providers/AntdProvider'
|
||||
import { ThemeProvider } from './providers/ThemeProvider'
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
@@ -23,8 +28,11 @@ function App(): JSX.Element {
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AgentsPage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/messages/*" element={<HistoryPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
|
||||
@@ -1,55 +1,88 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1722242729348') format('woff2'),
|
||||
url('iconfont.woff?t=1722242729348') format('woff'),
|
||||
url('iconfont.ttf?t=1722242729348') format('truetype');
|
||||
font-family: 'iconfont'; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1725606177995') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-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";
|
||||
content: '\e72f';
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: "\e6b7";
|
||||
content: '\e6b7';
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: "\e7de";
|
||||
content: '\e7de';
|
||||
}
|
||||
|
||||
.icon-history:before {
|
||||
content: "\e758";
|
||||
content: '\e758';
|
||||
}
|
||||
|
||||
.icon-hidesidebarhoriz:before {
|
||||
content: "\e8eb";
|
||||
.icon-hide-sidebar:before {
|
||||
content: '\e8eb';
|
||||
}
|
||||
|
||||
.icon-showsidebarhoriz:before {
|
||||
content: "\e944";
|
||||
.icon-show-sidebar:before {
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
.icon-a-addchat:before {
|
||||
content: "\e658";
|
||||
content: '\e658';
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: "\e792";
|
||||
content: '\e792';
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e615";
|
||||
content: '\e615';
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e78e";
|
||||
content: '\e78e';
|
||||
}
|
||||
|
||||
|
||||
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 |
18
src/renderer/src/assets/images/apps/bolt.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="black"/>
|
||||
<g filter="url(#filter0_i_2119_154)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.64368 11.7731C7.91976 11.7731 7.20901 11.5147 6.80099 10.9591L6.65707 11.6143L4 13L4.28684 11.6143L6.22186 3H8.59103L7.9066 6.03634C8.45941 5.44199 8.97273 5.22234 9.63083 5.22234C11.0523 5.22234 12 6.1397 12 7.81938C12 9.55074 10.9076 11.7731 8.64368 11.7731ZM9.55186 8.31036C9.55186 9.11144 8.97273 9.71871 8.22249 9.71871C7.8013 9.71871 7.4196 9.56366 7.16952 9.29233L7.53806 7.70309C7.81447 7.43176 8.13036 7.27671 8.49889 7.27671C9.06486 7.27671 9.55186 7.69017 9.55186 8.31036Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_2119_154" x="4" y="3" width="8" height="10" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.0192413"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.95 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2119_154"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 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/doubao.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/apps/felo.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src/renderer/src/assets/images/apps/gemini.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
14
src/renderer/src/assets/images/apps/huggingchat.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
|
||||
<path
|
||||
fill="#FFD21E"
|
||||
d="M4 15.55C4 9.72 8.72 5 14.55 5h4.11a9.34 9.34 0 1 1 0 18.68H7.58l-2.89 2.8a.41.41 0 0 1-.69-.3V15.55Z"
|
||||
/>
|
||||
<path
|
||||
fill="#32343D"
|
||||
d="M19.63 12.48c.37.14.52.9.9.7.71-.38.98-1.27.6-1.98a1.46 1.46 0 0 0-1.98-.61 1.47 1.47 0 0 0-.6 1.99c.17.34.74-.21 1.08-.1ZM12.72 12.48c-.37.14-.52.9-.9.7a1.47 1.47 0 0 1-.6-1.98 1.46 1.46 0 0 1 1.98-.61c.71.38.98 1.27.6 1.99-.18.34-.74-.21-1.08-.1ZM16.24 19.55c2.89 0 3.82-2.58 3.82-3.9 0-1.33-1.71.7-3.82.7-2.1 0-3.8-2.03-3.8-.7 0 1.32.92 3.9 3.8 3.9Z"
|
||||
/>
|
||||
<path
|
||||
fill="#FF323D"
|
||||
d="M18.56 18.8c-.57.44-1.33.75-2.32.75-.92 0-1.65-.27-2.2-.68.3-.63.87-1.11 1.55-1.32.12-.03.24.17.36.38.12.2.24.4.37.4s.26-.2.39-.4.26-.4.38-.36a2.56 2.56 0 0 1 1.47 1.23Z"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 810 B |
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/qingyan.png
Normal file
|
After Width: | Height: | Size: 11 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/wanzhi.jpg
Normal file
|
After Width: | Height: | Size: 2.0 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 |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 195 KiB |
55
src/renderer/src/assets/images/logo/cherry-hr.svg
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ea5e5d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #23af69;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ea5756;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_图层_1-2" data-name="图层_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
|
||||
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
|
||||
</g>
|
||||
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
|
||||
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
|
||||
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
|
||||
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
|
||||
<g>
|
||||
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
|
||||
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
|
||||
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
|
||||
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
|
||||
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
|
||||
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
|
||||
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
|
||||
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
|
||||
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
|
||||
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
|
||||
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
|
||||
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.5 KiB |
55
src/renderer/src/assets/images/logo/cherry-text.svg
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.39 115.44">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ea5e5d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #23af69;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ea5756;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_图层_1-2" data-name="图层_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1" d="M25.31,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
|
||||
<path class="cls-1" d="M40.64,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04c3.17-3.23,7.36-5.01,11.81-5.01s8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
|
||||
</g>
|
||||
<path class="cls-2" d="M40.64,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M10.19,90.22l.39-.28c.49-.29.83-.43,1.03-.43.45,0,.93.4,1.44,1.21.32.5.47.9.47,1.21s-.1.55-.29.75c-.19.2-.42.38-.68.54-.26.16-.51.31-.74.45-.24.14-.72.33-1.45.56-.73.23-1.44.34-2.12.34s-1.37-.09-2.07-.27c-.7-.18-1.41-.48-2.15-.9-.74-.42-1.4-.94-1.99-1.55-.59-.61-1.07-1.39-1.45-2.35-.38-.96-.57-1.99-.57-3.11s.19-2.14.56-3.05c.37-.91.85-1.67,1.43-2.26.58-.6,1.25-1.09,1.99-1.5,1.41-.78,2.82-1.16,4.24-1.16.67,0,1.36.1,2.06.31.7.21,1.22.42,1.58.64l.52.3c.26.16.46.29.6.39.37.3.56.64.56,1.02s-.15.78-.45,1.2c-.56.78-1.06,1.16-1.51,1.16-.26,0-.62-.16-1.1-.47-.6-.49-1.41-.73-2.41-.73-.93,0-1.85.32-2.76.97-.43.32-.79.76-1.08,1.34-.29.57-.43,1.22-.43,1.95s.14,1.37.43,1.95c.29.57.65,1.03,1.1,1.36.88.63,1.79.95,2.74.95.45,0,.87-.06,1.26-.17.39-.11.68-.23.85-.34Z"/>
|
||||
<path class="cls-3" d="M24.7,79.2c.11-.22.31-.37.58-.45.27-.09.62-.13,1.03-.13s.75.04.99.11c.24.07.43.16.56.26.13.1.23.24.3.43.07.24.11.62.11,1.12v11.95c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-4.37h-5.71v4.39c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v4.39h5.71v-4.42c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57Z"/>
|
||||
<path class="cls-3" d="M33.82,90.58h6.63c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-8.53c-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98c0-.73.14-1.23.41-1.5.27-.27.79-.4,1.55-.4h8.49c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-6.61v2.18h4.26c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.82,1.77-.24.09-.62.13-1.12.13h-4.22v2.18Z"/>
|
||||
<path class="cls-3" d="M83.34,79c.7.49,1.06.96,1.06,1.42,0,.27-.17.65-.5,1.14l-4.65,6.96v4.11c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.11.22-.31.37-.58.45-.27.09-.64.13-1.1.13s-.83-.04-1.1-.13c-.27-.09-.47-.24-.58-.46-.11-.22-.18-.42-.2-.58-.02-.17-.03-.42-.03-.76v-4.07l-4.65-6.96c-.33-.49-.5-.87-.5-1.14,0-.46.32-.9.95-1.32.63-.42,1.08-.64,1.36-.64s.49.06.65.17c.24.16.5.45.78.88l3.34,5.34,3.34-5.34c.27-.43.51-.71.71-.85s.43-.2.7-.2.69.18,1.26.54Z"/>
|
||||
<g>
|
||||
<path class="cls-3" d="M1.66,112.96c-.37-.46-.56-.87-.56-1.24s.31-.85.93-1.45c.36-.34.74-.52,1.14-.52s.96.36,1.68,1.08c.2.24.49.48.86.7.37.22.72.33,1.03.33,1.34,0,2-.55,2-1.64,0-.33-.18-.61-.55-.83-.37-.22-.82-.38-1.37-.48-.55-.1-1.13-.26-1.77-.48-.63-.22-1.22-.48-1.77-.79-.55-.3-1-.78-1.37-1.43-.37-.65-.55-1.44-.55-2.36,0-1.26.47-2.37,1.41-3.31s2.22-1.41,3.84-1.41c.86,0,1.65.11,2.36.33.71.22,1.2.45,1.48.68l.54.41c.45.42.67.77.67,1.06s-.17.68-.52,1.18c-.49.72-.99,1.08-1.51,1.08-.3,0-.67-.14-1.12-.43-.04-.03-.13-.1-.25-.22-.12-.11-.23-.21-.33-.28-.3-.19-.69-.28-1.15-.28s-.85.11-1.16.33c-.31.22-.46.53-.46.93s.18.71.55.96c.37.24.82.41,1.37.5.55.09,1.14.22,1.79.4.65.18,1.24.4,1.79.66.55.26,1,.71,1.37,1.35.37.64.55,1.42.55,2.36s-.19,1.76-.56,2.47c-.37.71-.86,1.26-1.46,1.65-1.16.76-2.4,1.14-3.73,1.14-.68,0-1.31-.08-1.92-.25-.6-.17-1.09-.37-1.46-.61-.76-.46-1.29-.9-1.59-1.34l-.19-.24Z"/>
|
||||
<path class="cls-3" d="M15.02,99.37h11.98c.46,0,.8.05,1.01.16.22.11.36.28.43.51.07.23.11.53.11.9s-.04.67-.11.89c-.07.22-.19.38-.37.46-.26.13-.62.19-1.1.19h-4.11v10.83c0,.33-.01.57-.03.73s-.09.34-.19.55c-.11.21-.3.36-.57.44-.27.09-.63.13-1.08.13s-.8-.04-1.07-.13c-.27-.09-.45-.23-.56-.44-.11-.21-.17-.4-.19-.56-.02-.17-.03-.41-.03-.74v-10.81h-4.14c-.46,0-.8-.05-1.01-.16-.22-.11-.36-.28-.43-.51-.07-.23-.11-.53-.11-.9s.04-.67.11-.89c.07-.22.19-.38.37-.46.26-.13.62-.19,1.1-.19Z"/>
|
||||
<path class="cls-3" d="M40.05,99.98c.14-.23.35-.39.62-.47.27-.09.61-.13,1.02-.13s.74.04.98.11c.24.07.43.16.56.26.13.1.22.25.28.45.09.24.13.62.13,1.12v6.5c0,1.9-.59,3.62-1.77,5.17-.57.73-1.31,1.32-2.22,1.78s-1.91.68-3,.68-2.1-.23-2.99-.69c-.9-.46-1.63-1.06-2.19-1.81-1.16-1.52-1.74-3.25-1.74-5.17v-6.48c0-.34.01-.6.03-.76.02-.17.09-.36.2-.57.11-.22.31-.37.58-.45.27-.09.64-.13,1.1-.13s.83.04,1.1.13c.27.09.46.24.56.45.17.33.26.78.26,1.36v6.46c0,.88.22,1.71.65,2.5.22.4.54.72.97.97.43.24.94.37,1.53.37,1.05,0,1.83-.39,2.35-1.16.52-.78.78-1.67.78-2.69v-6.59c0-.56.07-.95.22-1.18Z"/>
|
||||
<path class="cls-3" d="M47.28,99.37l3.98.02c2.08,0,3.91.75,5.49,2.25,1.58,1.5,2.37,3.35,2.37,5.54s-.77,4.07-2.32,5.63c-1.54,1.57-3.41,2.35-5.61,2.35h-3.94c-.88,0-1.42-.18-1.64-.54-.17-.3-.26-.76-.26-1.38v-11.98c0-.34.01-.6.03-.75s.09-.34.2-.56c.2-.39.76-.58,1.68-.58ZM51.27,111.35c1.03,0,1.97-.38,2.8-1.15.83-.77,1.25-1.73,1.25-2.9s-.41-2.14-1.22-2.92c-.81-.78-1.76-1.17-2.85-1.17h-2.07v8.14h2.09Z"/>
|
||||
<path class="cls-3" d="M60.53,101.29c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v11.98c0,.34-.01.6-.03.75s-.09.34-.2.56c-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98Z"/>
|
||||
<path class="cls-3" d="M73.47,99.2c2.13,0,3.97.77,5.54,2.3,1.57,1.54,2.35,3.44,2.35,5.72s-.75,4.21-2.24,5.82c-1.49,1.6-3.33,2.4-5.51,2.4s-4.04-.79-5.57-2.37c-1.53-1.58-2.29-3.46-2.29-5.64,0-1.19.22-2.31.65-3.35.43-1.04,1.01-1.91,1.72-2.62.72-.7,1.54-1.26,2.48-1.66.93-.4,1.9-.6,2.89-.6ZM69.55,107.32c0,1.28.41,2.32,1.24,3.11.83.8,1.75,1.2,2.77,1.2s1.94-.39,2.76-1.16c.82-.78,1.23-1.82,1.23-3.12s-.41-2.35-1.24-3.14c-.83-.79-1.75-1.18-2.77-1.18s-1.94.4-2.76,1.2c-.82.8-1.23,1.83-1.23,3.11Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M69.11,80.61c-.02-.17-.09-.36-.2-.57-.2-.39-.76-.58-1.68-.58h-4.65c-1.26,0-2.49.46-3.68,1.38-.57.45-1.05,1.05-1.42,1.81-.37.76-.56,1.61-.56,2.54,0,1.62.54,2.96,1.62,4.01-.32.76-.8,1.89-1.46,3.38-.22.52-.32.89-.32,1.12,0,.55.45,1.01,1.34,1.38.46.2.83.3,1.11.3s.51-.07.69-.2c.18-.14.31-.28.4-.42.14-.27.7-1.57,1.68-3.9l.67.04h2.71v2.43c0,.33.01.58.03.74.02.17.09.36.2.57.2.39.76.58,1.68.58,1.01,0,1.59-.27,1.77-.8.09-.24.13-.62.13-1.12v-11.95c0-.33-.01-.58-.03-.74ZM66.19,86.56c-.06.1-.34.54-.88.65-.14.03-.26.02-.34.02-.01.1-.03.19-.04.27-.11.59-.27.74-.4.76-.21.03-.36-.25-.66-.64-.16,0-.32.01-.49.01-.36,0-.72-.02-1.06-.06-.18-.02-.35-.05-.52-.08-.84-.22-1.44-.9-1.44-1.7v-.97c0-.8.61-1.48,1.44-1.7.17-.03.34-.06.52-.08.34-.04.69-.06,1.06-.06s.72.02,1.06.06c.18.02.35.05.52.08.43.12.8.35,1.06.66.24.29.39.65.39,1.05v.97c0,.16,0,.47-.21.78Z"/>
|
||||
<circle class="cls-2" cx="62.23" cy="85.3" r=".35"/>
|
||||
<circle class="cls-2" cx="63.4" cy="85.33" r=".35"/>
|
||||
<circle class="cls-2" cx="64.61" cy="85.33" r=".35"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M43.62,78.61c.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58h4.65c1.26,0,2.49.46,3.68,1.38.57.45,1.05,1.05,1.42,1.81.37.76.56,1.61.56,2.54,0,1.62-.54,2.96-1.62,4.01.32.76.8,1.89,1.46,3.38.22.52.32.89.32,1.12,0,.55-.45,1.01-1.34,1.38-.46.2-.83.3-1.11.3s-.51-.07-.69-.2c-.18-.14-.31-.28-.4-.42-.14-.27-.7-1.57-1.68-3.9l-.67.04h-2.71v2.43c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74ZM46.53,84.56c.06.1.34.54.88.65.14.03.26.02.34.02.01.1.03.19.04.27.11.59.27.74.4.76.21.03.36-.25.66-.64.16,0,.32.01.49.01.36,0,.72-.02,1.06-.06.18-.02.35-.05.52-.08.84-.22,1.44-.9,1.44-1.7v-.97c0-.8-.61-1.48-1.44-1.7-.17-.03-.34-.06-.52-.08-.34-.04-.69-.06-1.06-.06s-.72.02-1.06.06c-.18.02-.35.05-.52.08-.43.12-.8.35-1.06.66-.24.29-.39.65-.39,1.05v.97c0,.16,0,.47.21.78Z"/>
|
||||
<circle class="cls-2" cx="50.49" cy="83.3" r=".35"/>
|
||||
<circle class="cls-2" cx="49.32" cy="83.33" r=".35"/>
|
||||
<circle class="cls-2" cx="48.11" cy="83.33" r=".35"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src/renderer/src/assets/images/models/360.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
src/renderer/src/assets/images/models/360_dark.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/renderer/src/assets/images/models/adept.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |