Compare commits
1264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3499cd449b | |||
| a97c3d9695 | |||
| d0ddfce280 | |||
| 707e713e73 | |||
| 5347df4840 | |||
| 2ca0a62efa | |||
| 28c5231741 | |||
| 994ffa224e | |||
| ea990e78a5 | |||
| 6fd5ff991d | |||
| cd6c0a1f66 | |||
| 9145e998c4 | |||
| 8f1528b21c | |||
| d11f892c26 | |||
| 63b4ecbadd | |||
| f6cb501119 | |||
| 70ba8df57c | |||
| 89508162b7 | |||
| f107fb0c78 | |||
| a183a9a21e | |||
| dffcaa11c3 | |||
| 0fe7d559c8 | |||
| eef141cbe7 | |||
| 424eb09995 | |||
| c29cab7daa | |||
| 592484af95 | |||
| e9c9f3b488 | |||
| a2a3760c95 | |||
| 62de293194 | |||
| 9a65a1e7c7 | |||
| 82eb22d978 | |||
| ed1f80da00 | |||
| 11620828ad | |||
| b89213b1ab | |||
| 9ca46ee3d3 | |||
| 530bf42abb | |||
| c527fbdcd2 | |||
| cbb1173a3d | |||
| ae47d170ca | |||
| fd6e4db888 | |||
| ea31f27451 | |||
| 88143ba695 | |||
| 0ddcecabdf | |||
| f9f2586dc4 | |||
| e8ae776084 | |||
| 9655b33903 | |||
| 1a2a382916 | |||
| 16d9be4ce4 | |||
| 8374cd508d | |||
| e0ba3b8968 | |||
| 68acbe8f3d | |||
| 68d7815332 | |||
| 6ab0a89a98 | |||
| 15ab8407e4 | |||
| b50f8a4c11 | |||
| 359f6e36e9 | |||
| a04757c0d9 | |||
| ab8600864e | |||
| d22a101fd1 | |||
| 2d1ab70818 | |||
| 99ac5986ee | |||
| 9ae7c5101e | |||
| 889331005e | |||
| b3fbe35efe | |||
| 3fdbb5a9da | |||
| 5b0b36dc5c | |||
| 60eb08a982 | |||
| 570d6aeaf1 | |||
| 8df09b4ecc | |||
| d35f15574e | |||
| f21bf3d860 | |||
| 4dacea04a6 | |||
| 4939fc8b03 | |||
| b4c71b4dd3 | |||
| 6870390b9f | |||
| 495656ec9d | |||
| 1e4bc56780 | |||
| 48a6c4d017 | |||
| e5342cd414 | |||
| 2941aadd0f | |||
| 4597d2a930 | |||
| 456ad612aa | |||
| 899c183c5c | |||
| a83c153531 | |||
| 6a187fd370 | |||
| 827d5c58d0 | |||
| e11bb16307 | |||
| b316c3ae64 | |||
| 07ad7f0622 | |||
| 0863cfb2af | |||
| 0e44f9cd2a | |||
| e760b1be6b | |||
| 187726ae8d | |||
| 07199d0ed6 | |||
| a6921b064d | |||
| 486563062c | |||
| 7096f81234 | |||
| 94ba450323 | |||
| ed59e0f47e | |||
| 857bb02e50 | |||
| 1e830c0613 | |||
| 90077a519d | |||
| bb25522798 | |||
| e0e1d285e4 | |||
| 45c10fa166 | |||
| 295454a85e | |||
| b441d76991 | |||
| 555c5baafa | |||
| 8c5273d47d | |||
| e5f2fab43c | |||
| 62a8c28a6a | |||
| 7d048872e1 | |||
| 9cb127f14e | |||
| c8983f3000 | |||
| d8808b89f1 | |||
| 730b03cde8 | |||
| cef32f4b36 | |||
| 893a04aba3 | |||
| 43da80cba1 | |||
| a30cfb53bf | |||
| d1087ec87c | |||
| 3f285d0676 | |||
| 61829ab591 | |||
| fb30a796d7 | |||
| 604f76d55e | |||
| e8cba0ca01 | |||
| 8c3ce1a787 | |||
| 8faececa4c | |||
| aa6ecb4814 | |||
| 4c5b8ee0ee | |||
| 394483c363 | |||
| 6238b353cd | |||
| ea8de1f954 | |||
| 7df87d5eeb | |||
| 66ddab8ebf | |||
| 93ad07b44e | |||
| ca085a807e | |||
| 18d143f56e | |||
| c7bd1918a9 | |||
| d7cbba8f5b | |||
| 25f354c651 | |||
| e89e27b0d7 | |||
| 38b52a2ee6 | |||
| 442ef89ce0 | |||
| 762c901074 | |||
| 1b0b2f6736 | |||
| a39ff78758 | |||
| 18b7618a8d | |||
| 008bb33013 | |||
| 6b1c27ab2c | |||
| 52de270d04 | |||
| a0fde96b40 | |||
| 8a3bf652d3 | |||
| 8b2c1cbe99 | |||
| f8361d50e7 | |||
| 541405d708 | |||
| c2ff5f3997 | |||
| fdb856199a | |||
| 866ce86cc0 | |||
| 145be1fd87 | |||
| 6f973741a2 | |||
| 98937310d3 | |||
| 58412aecde | |||
| 2392bb4ed4 | |||
| a2aa7aed09 | |||
| e45aca2343 | |||
| 083d1b5550 | |||
| acc0d3e01f | |||
| fb27be0f59 | |||
| 7122d44b13 | |||
| 9d627e660f | |||
| 42b8b696a2 | |||
| bac4dcf73c | |||
| 06ab8f35ce | |||
| 84360bfde8 | |||
| 157146151e | |||
| 647ecbfa61 | |||
| 1de54caa7e | |||
| abecb74135 | |||
| aae12a21ac | |||
| aa75f90294 | |||
| 723e686455 | |||
| 03c18287fc | |||
| 01f7faff8a | |||
| c13d584010 | |||
| dbf331b9b4 | |||
| 38c8327cbf | |||
| 0e5411d3ba | |||
| ee653b1032 | |||
| f5d3c07161 | |||
| 12d40713a9 | |||
| 151a08d0dd | |||
| 74567d5e17 | |||
| 85160c2d29 | |||
| 4634c88f76 | |||
| 2c21553059 | |||
| 8227e2553e | |||
| c69c750144 | |||
| a25c0e657b | |||
| 67bb1f19f0 | |||
| 7d7f9eaa35 | |||
| 92ed848d4e | |||
| 6bcc21c578 | |||
| 48d824fe6f | |||
| db2b92421a | |||
| 632b0c17aa | |||
| e61618f1b4 | |||
| 2fd3ebb378 | |||
| 56dd2d17e7 | |||
| cf92752e79 | |||
| 3a6d49d3fc | |||
| 9b79051ea5 | |||
| b9d97e8a35 | |||
| 89f1de4df4 | |||
| 4ca2d7f9dc | |||
| 75eb6680d8 | |||
| 53892fa5e6 | |||
| 3947cf07ec | |||
| c0e85b6caf | |||
| a090984c67 | |||
| 2d2a9ea299 | |||
| 3ccb06652d | |||
| 68685511e7 | |||
| 3791f30d8f | |||
| 0250ec6f2e | |||
| d98f9909db | |||
| 3c310c61d8 | |||
| 8a0a109fb2 | |||
| 3790e82ef3 | |||
| 7fb6fcdeeb | |||
| 7ce55cf90f | |||
| 4a8dcb2c08 | |||
| 2b34150ef7 | |||
| f429f6c39e | |||
| dcc90cd79f | |||
| 702568502e | |||
| db636e4b5a | |||
| 5c4f0e8e8e | |||
| 8e36d29996 | |||
| 02604c466d | |||
| 219cea0c53 | |||
| 08e75c39c0 | |||
| 647fa21e7c | |||
| bdf85c68d1 | |||
| a4c0224ab5 | |||
| 9e9c954560 | |||
| 262213cc8b | |||
| b42da9f154 | |||
| f890da0cda | |||
| 670d66b01d | |||
| de1ad09900 | |||
| 40163e5c63 | |||
| a8941326dc | |||
| 9c9f200874 | |||
| aa33f0242a | |||
| 2fc7c4b5c7 | |||
| 21b532f581 | |||
| 1978cfc356 | |||
| 37ee092398 | |||
| 85bf4498c0 | |||
| 3312befe11 | |||
| 49d29d78da | |||
| 3f82a692a2 | |||
| c44f3b8a3d | |||
| 37d172dbd9 | |||
| 8aba2f58c5 | |||
| 2db5b3d72f | |||
| 9afc6989af | |||
| 4a06c86412 | |||
| 602a6a5f66 | |||
| d714a53dc6 | |||
| a8451b7c3d | |||
| a0351fb5ad | |||
| f29eeeac9e | |||
| 371d38a9ee | |||
| ebef970078 | |||
| 2d8d478e2c | |||
| 3ba16118b4 | |||
| 94e0559dd3 | |||
| 0fdb2ed0ef | |||
| 2cf67b59d2 | |||
| 910bd30b24 | |||
| 754693f403 | |||
| e066db763a | |||
| 219dc2c8bf | |||
| a4b5ef9bde | |||
| e5664048d9 | |||
| f24177d5c4 | |||
| 48f66e785b | |||
| bdb6e30c92 | |||
| 062baad682 | |||
| 40182befe9 | |||
| 026f88d1b3 | |||
| 32749d65a4 | |||
| 46c7d35bb8 | |||
| c6f036cba5 | |||
| e656db779e | |||
| fa32cd13cf | |||
| a1ae55b29d | |||
| 05b3810d4a | |||
| c95c7faa5f | |||
| ed23e9395c | |||
| 1738a74e8c | |||
| 70eb0a9187 | |||
| a25f4e90dd | |||
| 3d701b98aa | |||
| dcaac54c75 | |||
| b2b89a1339 | |||
| 4692f98770 | |||
| 86a3a108a7 | |||
| 5ec4403bfb | |||
| ec0be1ff27 | |||
| 516315ac45 | |||
| ff55739376 | |||
| 1e24b7bc45 | |||
| dd92dca34b | |||
| a592fdc550 | |||
| 846e7ca097 | |||
| 93c2a94658 | |||
| 309b66e4df | |||
| 4d9476e99b | |||
| 46f796a74c | |||
| 00bf28b999 | |||
| 640d3783a0 | |||
| 0e4f06e86a | |||
| 886a7ec1e9 | |||
| 37cf7427f9 | |||
| e69d0c89a6 | |||
| 581e2fb786 | |||
| 13b465fe73 | |||
| a12d10f4f7 | |||
| e8bfb2b49b | |||
| ae995182b2 | |||
| 59c69e065c | |||
| 4ca2d61ccc | |||
| d62ff69351 | |||
| 012e79a7e2 | |||
| 97dc80a07f | |||
| b974f8537f | |||
| c32e17968e | |||
| cf09d1d44d | |||
| ad39d8774d | |||
| 687f140a5c | |||
| 1b09bb47bf | |||
| 808b457503 | |||
| 92e054569c | |||
| 2f22e68559 | |||
| 53a8628fab | |||
| df04503674 | |||
| 1fe74fa753 | |||
| 55a9be2fa5 | |||
| fdf4821d56 | |||
| 84e6caa846 | |||
| 3bc8dfdf8c | |||
| ed96940e82 | |||
| 1c60375d71 | |||
| c9699609ed | |||
| f91caff7ec | |||
| 11bd55701c | |||
| efa9c6c546 | |||
| 92ab67eb3d | |||
| ae11490f87 | |||
| 956c2f683d | |||
| d01f793558 | |||
| 94d9b79957 | |||
| 78a7b2759e | |||
| 27c0edfb79 | |||
| 59b1d8bcc4 | |||
| 741d84b4d3 | |||
| 5bacf048f2 | |||
| 1d4916c516 | |||
| 8e1207c2a2 | |||
| ac92f1a783 | |||
| 28c59ea436 | |||
| 9e808208ab | |||
| feefaaf3e3 | |||
| 31078b8ec5 | |||
| f3f32cc591 | |||
| f489b034b5 | |||
| 3d9d5b6263 | |||
| 89440c9c10 | |||
| 4c0f358323 | |||
| ad01fc43e5 | |||
| 9b17416f9c | |||
| cda4edfb7f | |||
| acc803aa43 | |||
| 2ab8f325df | |||
| a68cbe4438 | |||
| 646d0e4ccb | |||
| a7a82be083 | |||
| c0117c25ac | |||
| d51da99b8f | |||
| d4848faa5a | |||
| bfeca0b383 | |||
| 79c7c3dc1c | |||
| b2ebbc1e30 | |||
| 50e2dd0ec0 | |||
| 5e753de71c | |||
| 6bc6dab879 | |||
| 7d794d33dd | |||
| aab318e8ca | |||
| 6554a3817b | |||
| 7d76db40e8 | |||
| 186c82e355 | |||
| 67311f1cbe | |||
| 62d969335e | |||
| c6eb77ab8b | |||
| 7e17987fa3 | |||
| 4bc69b7c5e | |||
| e08029a6f5 | |||
| 93d68102d6 | |||
| f448d8a8db | |||
| a047048f69 | |||
| aec14567ee | |||
| bad89e3d28 | |||
| 408f2b16ad | |||
| d6b87ece23 | |||
| 91104e288c | |||
| aeeded2aa1 | |||
| b10198de1f | |||
| 0789ccedbb | |||
| 0a5401174b | |||
| 69513cc76e | |||
| 5a471125db | |||
| 06b2ca9149 | |||
| 26dd931f70 | |||
| 68df5cd211 | |||
| c7071a98f0 | |||
| ff14dcc559 | |||
| bb02ca83dc | |||
| 4d9e842381 | |||
| 0165bcdce3 | |||
| f015c78060 | |||
| c233ba0a1c | |||
| 88dd75827a | |||
| 2ab63f2e4c | |||
| 91bf356c73 | |||
| 28c0748001 | |||
| 3108a1c0b3 | |||
| 6cfa7d0eb6 | |||
| da6c80ebc2 | |||
| af1a9868db | |||
| f87ba144c8 | |||
| 8d61cbcae9 | |||
| c61dde5085 | |||
| 3015e90925 | |||
| b3629e83f2 | |||
| cef9312e7e | |||
| 802622646a | |||
| ce70c7239c | |||
| c354537f30 | |||
| fb6b0b0c97 | |||
| fc59144b1d | |||
| cacd0a1387 | |||
| af9763d142 | |||
| b9402a8370 | |||
| 97a08f00a3 | |||
| 4e20bd1ef8 | |||
| 5c2d936688 | |||
| 6b34aac263 | |||
| f59f6ade69 | |||
| 50478c600f | |||
| bba5cac246 | |||
| b9b31aed52 | |||
| 40203fb721 | |||
| b83343a8b9 | |||
| 7677850547 | |||
| d0e233f1b3 | |||
| 903d0043ba | |||
| 811815d69d | |||
| d5b9c35f0a | |||
| 83c8f06b81 | |||
| acb2ea30fb | |||
| 55317b5608 | |||
| b42a5c5e63 | |||
| cc76fe19f9 | |||
| cf2d7ba8b4 | |||
| 1c163c55b8 | |||
| 69bb661b5a | |||
| f062c56de4 | |||
| 4c9bd02f8e | |||
| 241cb0c0d8 | |||
| 6a57973864 | |||
| 369f629206 | |||
| 09a8f83650 | |||
| 40912eaaf4 | |||
| 0d236a94ab | |||
| 3a936e0f26 | |||
| 81a35d129d | |||
| e2d0c3bbce | |||
| 12c9d810a2 | |||
| b18b161094 | |||
| 32da853f27 | |||
| bf51a0b5c6 | |||
| ae71a7be9e | |||
| 0fb6795833 | |||
| dd6d228760 | |||
| 8c5999dc82 | |||
| 31b0fbf775 | |||
| 39fe583030 | |||
| 5c19695e21 | |||
| 6e4610e337 | |||
| eb2439b90c | |||
| a4a0980cd3 | |||
| 7d99765589 | |||
| 5f6cf1bd66 | |||
| 02930a2793 | |||
| b31b1c7908 | |||
| d0ee764732 | |||
| 01cd10b364 | |||
| 29ba156b9a | |||
| e541c7b429 | |||
| 5f4142f0c4 | |||
| 95bbc70c93 | |||
| 4add56ae6a | |||
| 16f87537a2 | |||
| 7f05626a8f | |||
| 2094e2201a | |||
| e0fcdf43c5 | |||
| affc866c17 | |||
| 799267049f | |||
| cb8d47a17b | |||
| c494288f7b | |||
| 2c3f89dbde | |||
| 4721a660fa | |||
| 6aaa3def0d | |||
| 045708d9b3 | |||
| 9ffe92d378 | |||
| 7159481217 | |||
| d07e136037 | |||
| b38a9c954a | |||
| 7139d5093a | |||
| 9e283d6930 | |||
| c9a4e12765 | |||
| 7bd644451b | |||
| 5a00bdcbc6 | |||
| 3c958c3d11 | |||
| 1d5ace0fb2 | |||
| f8fce871da | |||
| de76d3fedc | |||
| b2c6662192 | |||
| bf8a7c01b0 | |||
| fb8ed35b59 | |||
| 7c4d81c108 | |||
| 7199f73e06 | |||
| 869e56b53c | |||
| f99851fb6b | |||
| c94450db44 | |||
| 195ef92acc | |||
| a67370426b | |||
| 9d35205681 | |||
| 98087e50db | |||
| bedac4f59d | |||
| aba3874797 | |||
| 3383280726 | |||
| 0c13e708b9 | |||
| bc77c423b3 | |||
| 4821756301 | |||
| 78290ca70e | |||
| 7feeb07624 | |||
| 93e28ed916 | |||
| b4aaf052fe | |||
| b37e0389fc | |||
| e1ebe069a5 | |||
| d73912ee3b | |||
| f81c7c7a6c | |||
| 5a7bcd5997 | |||
| 09a347cae4 | |||
| 266f909045 | |||
| bad2f15c1f | |||
| e3115d00bf | |||
| 0c0ccf3d11 | |||
| 2076e6f998 | |||
| b49d80b78d | |||
| ab5e830ed1 | |||
| e0eca97053 | |||
| d175212d9a | |||
| 642ce160a1 | |||
| 574d02a8c9 | |||
| 7764507d74 | |||
| fa8bf61532 | |||
| 30e8cef9cc | |||
| 1a2861e81a | |||
| 653e5d82ed | |||
| 5be0e0ae72 | |||
| b92b46f2b0 | |||
| 23686d4926 | |||
| b340b40bcf | |||
| 253fc6f4e1 | |||
| 99aa0d3255 | |||
| 23a2a6b57c | |||
| a869857fc1 | |||
| 4ecedcb267 | |||
| cbd6a30e14 | |||
| 5f2cddee09 | |||
| c0e0e924f7 | |||
| b6ad7eeb9a | |||
| 9cf74317a6 | |||
| 82fcc2292e | |||
| 4eb0c25682 | |||
| 9e128d2524 | |||
| 1473cb3123 | |||
| 2c5fe01fbf | |||
| d574a09529 | |||
| f20bccfd7d | |||
| 5dcc892f31 | |||
| 26e3871688 | |||
| 9a6aad35b0 | |||
| 16feb49e9e | |||
| 30959e2380 | |||
| 2c17f75f4f | |||
| 2d1a930bfe | |||
| 320d27059f | |||
| 31014aa8a6 | |||
| b468ecfce7 | |||
| c53d63f7af | |||
| dabff0a847 | |||
| 26a5ae0086 | |||
| 88e0d293a2 | |||
| 0c97b52c53 | |||
| 2449a22c69 | |||
| 028f9d88d9 | |||
| a07c6cdffb | |||
| 5a647b0d61 | |||
| 007e6419ba | |||
| caa473639c | |||
| b6825a6ea2 | |||
| 710180997f | |||
| fd4334f331 | |||
| 80dedc149a | |||
| 8eacaa281a | |||
| 6e75140939 | |||
| 5a3a97135f | |||
| 44d42d64ef | |||
| fad3f67678 | |||
| 65b30b3b0d | |||
| 0278228a84 | |||
| bb0cb1cecc | |||
| f5cd6ecb50 | |||
| 76c0ad9985 | |||
| 4c771c6913 | |||
| 15d50761e7 | |||
| 1c33c90884 | |||
| 53e5a3bf76 | |||
| adfea253d7 | |||
| 6c17fee69e | |||
| 40958ffb2c | |||
| 22d2121dcc | |||
| 4632f1a92a | |||
| b9affe3eb8 | |||
| c6f136caa2 | |||
| 827959e580 | |||
| 8739c49634 | |||
| e99f253d48 | |||
| 0cb82bd3bd | |||
| cf89981b64 | |||
| e4133b0054 | |||
| 9249ee9094 | |||
| 0a4b360745 | |||
| 59c4d60d6a | |||
| dcdb00eee7 | |||
| 946129ceb3 | |||
| ada45b229d | |||
| 7934ce473d | |||
| f749bef2fd | |||
| 6b2452422e | |||
| 50438dd612 | |||
| 38f665e484 | |||
| 635e125ef4 | |||
| 1b3ae92854 | |||
| ce66f2e2ea | |||
| 726efe3558 | |||
| 24deb56d00 | |||
| 5e8d7682f5 | |||
| ceb97e80ff | |||
| bf1fa5b767 | |||
| e99f34893e | |||
| a49dd6101e | |||
| d03b852671 | |||
| 13e48411c1 | |||
| de1976d984 | |||
| 220046cc95 | |||
| bae76f921b | |||
| cb88a48d8b | |||
| 4d13a8d9c2 | |||
| 50cc1c6b5a | |||
| 1d82552491 | |||
| 5c2129c0c8 | |||
| 0eead315d8 | |||
| 04cfe5019e | |||
| a0cfe7df4a | |||
| 3d8748a61a | |||
| f3940159b3 | |||
| 81d4accacf | |||
| c900a186b7 | |||
| 72d61ef853 | |||
| 8c32f51892 | |||
| 0a3ad04f12 | |||
| 555a991a30 | |||
| 6ba6357d21 | |||
| cb3db57d2f | |||
| e1a04030b5 | |||
| 220d11a414 | |||
| 3d2e209550 | |||
| ee46d2055a | |||
| 3049023266 | |||
| 6fb79c17d2 | |||
| 9efc196ec5 | |||
| 186a1612e8 | |||
| 22920204d1 | |||
| d4efbbb1bf | |||
| 3f7e84e17c | |||
| 6df0b02e49 | |||
| 280ec3377b | |||
| 6a30eec5b4 | |||
| a6497b8c98 | |||
| acda36ae3f | |||
| 496b4684ea | |||
| 2898215a00 | |||
| 762c3d4950 | |||
| cec5eb3989 | |||
| 61ebe97a5a | |||
| 609a80bf2d | |||
| 468423040f | |||
| 85efc6e96b | |||
| 9f19493b41 | |||
| 3e43887be8 | |||
| bffc9d23aa | |||
| 402a3e5ef9 | |||
| ce2c8c7495 | |||
| dc6d79366e | |||
| 777455f167 | |||
| 65cc51ea94 | |||
| 95936dca2a | |||
| 9aa829e6fc | |||
| a27150c154 | |||
| 9acae0a728 | |||
| 9024d48938 | |||
| 02080954bc | |||
| abb922a2b1 | |||
| a1b88758cc | |||
| 3d16c735d9 | |||
| e74391562b | |||
| 53f46218d3 | |||
| 333547df3d | |||
| 4c877fb0a3 | |||
| bfa61ae3ee | |||
| 2208ab7277 | |||
| 507efda688 | |||
| 0914df8908 | |||
| 205aa70825 | |||
| 6c51e1d756 | |||
| 8e58dab337 | |||
| e171715490 | |||
| bda00e0a90 | |||
| b56d00a7e2 | |||
| e520db6949 | |||
| de141c8127 | |||
| 3f3259784b | |||
| 66c2c530c5 | |||
| d12fc29515 | |||
| 44991edfbd | |||
| d33714ad68 | |||
| e50223d219 | |||
| 4128b075e1 | |||
| a1f0c039b1 | |||
| 76abe08d1b | |||
| 1355dfe744 | |||
| 791d29cc41 | |||
| 88e76da466 | |||
| 250aa7154a | |||
| 790caae2ab | |||
| 7f7300e6dc | |||
| 4464992873 | |||
| 37d1c250d2 | |||
| e9c51579a2 | |||
| aec2952780 | |||
| 95a1bdac72 | |||
| 306cb04ef0 | |||
| dc9444a9d4 | |||
| ad9fefe902 | |||
| e07d4838a9 | |||
| 30d070040c | |||
| f335699958 | |||
| b1bc576e3f | |||
| a6f086e3be | |||
| 084da9ebab | |||
| 57aef23741 | |||
| 900b11bdf7 | |||
| 8aec8a60b3 | |||
| a566b0e91a | |||
| 4d201059ad | |||
| 00d91ecf01 | |||
| 462ac39897 | |||
| 3fa1e8c842 | |||
| d32a76c087 | |||
| 9e9fd37bda | |||
| dd464db594 | |||
| ccac5358f4 | |||
| e72e324155 | |||
| 28c18b6651 | |||
| 3d432d810f | |||
| 21ad28ee62 | |||
| f7db1289e4 | |||
| f5c547cdb2 | |||
| 9160cee919 | |||
| 298bb8be29 | |||
| b800c64fed | |||
| 504d7b88d4 | |||
| 713d6dba8f | |||
| a6833d5994 | |||
| d850fd315a | |||
| c04fd62bec | |||
| f86a274cd3 | |||
| 798a6e8c3e | |||
| 749353f460 | |||
| c510f5dcce | |||
| 46b314303c | |||
| b01aca9066 | |||
| 725f81c165 | |||
| c0e25879e5 | |||
| 4c22c404ca | |||
| 63673ec39f | |||
| 88cc783a95 | |||
| 9c55b4516c | |||
| aecc5fefcf | |||
| afc2e2f595 | |||
| 67b63ee07a | |||
| fd7132cd3a | |||
| a7d9700f06 | |||
| d9bb552f3f | |||
| ad2713c0be | |||
| 1e756614f9 | |||
| d457dfa3d3 | |||
| b24d88dfe3 | |||
| b6d598c52e | |||
| 67e1dd56e9 | |||
| 8b5dd427d0 | |||
| 4f44afeec4 | |||
| c46219cd6c | |||
| 999bd802c4 | |||
| 2300cca070 | |||
| b4de6292c3 | |||
| 42908e8834 | |||
| 57718dda6f | |||
| c87e88a53a | |||
| 5b00c21f15 | |||
| 6276890e5b | |||
| a7337ed4b0 | |||
| fe0f6318c9 | |||
| 75742323ea | |||
| f7f8c6f0c6 | |||
| e4f4c6cd86 | |||
| 8eac836e05 | |||
| a6795289da | |||
| eff639ddf9 | |||
| a046cf32ba | |||
| 66bc9cb3f9 | |||
| 247d1a1846 | |||
| 0e7fb2b19c | |||
| 8a94bb05ea | |||
| bc454d4dec | |||
| d388aeecfb | |||
| 3e33ee6cc5 | |||
| 1991df18d2 | |||
| de3206b052 | |||
| cb3ed42846 | |||
| edbc8560cc | |||
| 56761d6f69 | |||
| 2b4cfe7cb1 | |||
| 6a5faa6610 | |||
| 84979a975c | |||
| 74740d7fcc | |||
| dff04187be | |||
| a0a13a4015 | |||
| 2ad6a1f24c | |||
| cf7c0fc1fc | |||
| 4ecbf3edab | |||
| 83cc4ccec7 | |||
| 3998ad08de | |||
| 49a5bc7900 | |||
| 7633d70435 | |||
| ad9fb9aa6d | |||
| fc3d15fae8 | |||
| c45fc2bbad | |||
| 270216f461 | |||
| 112e90c15c | |||
| c579eff86e | |||
| f9f5befc59 | |||
| 7271a86677 | |||
| 42ede42f62 | |||
| ea7a42f736 | |||
| d2836826e7 | |||
| 7d61af7170 | |||
| 3f4fa9b0ec | |||
| 1bdf6c7955 | |||
| 5d005cf5a7 | |||
| 1fbd727a7b | |||
| c9813bb1e2 | |||
| edac2004a0 | |||
| a051f9fa44 | |||
| a70e69caf9 | |||
| 4896db93fd | |||
| 2e7ecbc753 | |||
| f68bd4d8d8 | |||
| d0948e6f8a | |||
| ac9017c031 | |||
| de1d79abb8 | |||
| ad577818dd | |||
| bb50447a98 | |||
| 158f9bf1ad | |||
| 6a9bc103d7 | |||
| 529ec3612e | |||
| d241c38c61 | |||
| ee5ed8c565 | |||
| dc73661678 | |||
| ce973ce3a0 | |||
| a0413158c8 | |||
| 6cb3b16451 | |||
| 08b0990cf9 | |||
| 10b9940edd | |||
| 4cbdd563e8 | |||
| dba1f76db7 | |||
| 15fb605eb4 | |||
| 1bf147fa6a | |||
| a782b2b4aa | |||
| 7f92cb59a6 | |||
| 6009ae84fb | |||
| 038aa2d5cc | |||
| 6384525e20 | |||
| 3fc7911c97 | |||
| 5f55d8c22c | |||
| d9f7bcfc21 | |||
| aa72794967 | |||
| 09e6756efe | |||
| dde0400f0d | |||
| 1d3a01dd49 | |||
| 63cdc15bc2 | |||
| b2818f8619 | |||
| 8ef9fb0216 | |||
| 63488e6fab | |||
| 6d9013f0a1 | |||
| 1a68587684 | |||
| 47c455b125 | |||
| 96124cf58e | |||
| ef975add01 | |||
| ed49066bab | |||
| e7545c5a94 | |||
| fc35df65b8 | |||
| 56ca81d245 | |||
| 6bc1f4b640 | |||
| ccb216e76a | |||
| 60931b85ff | |||
| dc1dbc7bb6 | |||
| 5d2efbd62b | |||
| 5337017648 | |||
| c409256ae9 | |||
| 4ac608052c | |||
| 5e6aaabb23 | |||
| 8812daeeee | |||
| 13e3a8478c | |||
| 8687985ccb | |||
| 7d54f9b4fa | |||
| 6b7ba35183 | |||
| 5b42a6d054 | |||
| 153e7a9299 | |||
| 77e0c5172e | |||
| c50ac440c8 | |||
| 34ebab0af8 | |||
| b85765915e | |||
| 960f50e4e4 | |||
| 65e19d187c | |||
| aa4f94f8a4 | |||
| aa3812eddc | |||
| 6b9e58171b | |||
| 2f64653b1e | |||
| 03dd3038e0 | |||
| f1f7e8e11b | |||
| fbd189c5e1 | |||
| 87c3716f75 | |||
| 37477587b6 | |||
| d558572d97 | |||
| 7506d04c55 | |||
| 35fd5aef22 | |||
| 8f11d2b1c9 | |||
| 9aa2a4727d | |||
| ca6027dd83 | |||
| c2462fd51c | |||
| 0739758469 | |||
| b2554333a9 | |||
| 6ced973b35 | |||
| ccbeefc546 | |||
| 7fdc2db522 | |||
| 978f1342e4 | |||
| ff935a656e | |||
| 15539a5609 | |||
| 88cd4f2144 | |||
| daf2e035b2 | |||
| 7ceb4920ec | |||
| 0074d5c8b4 | |||
| 96737ed695 | |||
| 356da1ea67 | |||
| debf996146 | |||
| 8d73d1e844 | |||
| b0d777293b | |||
| 1a9fbbc0a2 | |||
| ab99a7b96d | |||
| 7d561dbfb7 | |||
| 6af07c278d | |||
| 9c18b851cc | |||
| b1ebe13b5f | |||
| 9b258734c4 | |||
| 25eb97902b | |||
| 2fae6e4a3e | |||
| f312c5fc40 | |||
| afa96549a3 | |||
| 6beee78ce8 | |||
| a230ee2c69 | |||
| 28a27447a5 | |||
| 408976e5dc | |||
| 7153996c35 | |||
| 73f6a743cd | |||
| 3b250d7d78 | |||
| 272efaf76e | |||
| 44c64a571a | |||
| f817d9136b | |||
| c0f192c6f2 | |||
| b5a109401c | |||
| aeff59946c | |||
| 21ad4cfecc | |||
| 4df39179bb | |||
| 423fdb6992 | |||
| f66adcd217 | |||
| 465bf4006c | |||
| 14c9cb6001 | |||
| e35d928bcd | |||
| 1981f2e648 | |||
| e5c1791135 | |||
| ae1960f5c6 | |||
| 51ca9cb289 | |||
| 7d2df1a8c5 | |||
| 2757535cf0 | |||
| 243065221d | |||
| 2a674c169e | |||
| 100dbc8101 | |||
| 67d7ccbf10 | |||
| a71782abb6 | |||
| 73973ecb7f | |||
| 368de84440 | |||
| a170dbd6f0 | |||
| 9b84176a42 | |||
| 0f36610e23 | |||
| 1e273834b8 | |||
| 3b569131a5 | |||
| 115f111071 | |||
| a4d1bcffd9 | |||
| f5d37a4e53 | |||
| a9d4a0885c | |||
| 6596497c97 | |||
| 12d8f57dab | |||
| 7f2f3ad88a | |||
| cd3c053f81 | |||
| 7dacd58821 | |||
| 744a6ac7cb | |||
| 2e9041c891 | |||
| 3717ff25bf | |||
| 494d52ac85 | |||
| 22d2ff1518 | |||
| 06ae4328ea | |||
| 8de1197557 | |||
| 09e86b35a5 | |||
| 76ea170a01 | |||
| 4cd962b42f | |||
| 1cae86f93d | |||
| 1171100417 | |||
| dcf57651fe | |||
| 603b867a5f | |||
| e765bf9828 | |||
| 33d5da7325 | |||
| 383e8255a0 | |||
| 4cb5c128bb | |||
| 949a13b021 | |||
| e6c9cb60dc | |||
| ad625b23a7 | |||
| 2e34b79f26 | |||
| 9288e7b292 | |||
| ec703852f8 | |||
| fca9fb0c84 | |||
| 64c8831530 | |||
| 75e396ecf0 | |||
| 697c3b1838 | |||
| efa0f4cbdb | |||
| c3414e9b6d | |||
| 6ba6108f43 | |||
| c3d007b52c | |||
| e1494d408f | |||
| cd625430b2 | |||
| aefb08965d | |||
| d2dd70000b | |||
| f0a96bb34c | |||
| 0ec61e1c47 | |||
| 335ce4963b | |||
| 63ef0d2df1 | |||
| c0c0e8ae33 | |||
| 771b078df9 | |||
| 64e70ea918 | |||
| 2d46a4494e | |||
| 9b7e2282fe | |||
| 535b7d0a92 | |||
| 223496192d | |||
| db779446f0 | |||
| 8ef3ef2a8f | |||
| 30da183578 | |||
| 49c09f381c | |||
| c8c58ddcfb | |||
| 5bffb86d4f | |||
| 84fa5b065b | |||
| 7ecb35dfa7 | |||
| 2d7d403b15 | |||
| 7342a0afef | |||
| 1b8a3885f7 | |||
| c33c0b20f2 | |||
| 4f75f29361 | |||
| fe00eed7b9 | |||
| 30fa9277ff | |||
| 11a446e106 | |||
| d2ca6f1041 | |||
| 0f3dc87d08 | |||
| 7d3cae1f5b | |||
| ceae1fa3d0 | |||
| 12a2c8c86d | |||
| 29d6c4be18 | |||
| 738e51c078 | |||
| db050c002a | |||
| 398f995cd1 | |||
| 348fc365fa | |||
| 7b6d38e349 | |||
| 6a35c0e3d8 | |||
| 9a63169a73 | |||
| a9aa5a8da0 | |||
| a2d568175b | |||
| 0b9717780d | |||
| b371fed814 | |||
| 3311f8cdef | |||
| 422baa848b | |||
| 739aa21475 | |||
| 23ef4ab952 | |||
| b77f845cb0 | |||
| 0573b274ed | |||
| 60433bb1ab | |||
| 1caf53fbda | |||
| 446e011c6a | |||
| c319b54a26 | |||
| e9ca1d54a0 | |||
| 1ff8fe0c2e | |||
| 902341bc1d | |||
| 17fff46024 | |||
| d258c1cfe2 | |||
| 79cabadfb8 | |||
| 61ceca2363 | |||
| 8a8deda002 | |||
| 6536ec227a | |||
| af1fd90118 | |||
| 68fa2bad15 | |||
| bac3bad8db | |||
| e11633310c | |||
| 612b39a878 | |||
| 8491141edc | |||
| 088628f89f | |||
| a6b4e48640 | |||
| ba0e2c5848 | |||
| d986087857 | |||
| 73c93c5581 | |||
| ceec4a9f97 | |||
| cf08467552 | |||
| 34c85e8f0c | |||
| 1db3faa2a8 | |||
| 35efada37e | |||
| ceca3408ff | |||
| f2def559d4 | |||
| cd97be0f10 | |||
| b87394ed88 | |||
| a4d8e71916 | |||
| 39cf227e42 | |||
| 5d01d12d2a | |||
| d2cad31db4 | |||
| d7f4e4584a | |||
| eda870f181 | |||
| 3f093a91be | |||
| aa864f3876 | |||
| 77a8b23d76 | |||
| 45dd76e281 | |||
| 568d4814e3 | |||
| 9468f3b511 | |||
| 04af940144 | |||
| e33d9ac0ae | |||
| cd835b7c36 | |||
| dd4239da87 | |||
| 41c3895da4 | |||
| 2e9c7d0830 | |||
| 8ea73e14c9 | |||
| 3791556b13 | |||
| e0dab5cf5b | |||
| 1785e7df0a | |||
| 6cb1846b23 | |||
| 21243579b3 | |||
| 0d2ad2e4c3 | |||
| 071a3950cd | |||
| dc6066b74c | |||
| ce55d8d0e7 | |||
| d4ae321cd2 | |||
| 89dd35c98d | |||
| b8c70a3061 | |||
| 968a749aaa | |||
| e2fc593624 | |||
| 0e1674ce6c | |||
| 18566989be | |||
| 31fa10f185 | |||
| f6aa0dc55a | |||
| ca2a9ed84a | |||
| 79f6d598ab | |||
| fb564733e4 | |||
| 63e5972dd2 | |||
| b80270709f | |||
| d7b459dcee | |||
| 76b9e1a65e | |||
| b148c5adf5 | |||
| 2313f66ad9 | |||
| 02edd983d1 | |||
| 3e049baaa4 | |||
| 7401d85825 | |||
| 241dcddfed | |||
| cd0ea8154d | |||
| 6d6788eeb2 | |||
| 9ac35ae3d8 | |||
| 72e847258d | |||
| 0cc460a4a3 | |||
| 98307d5d85 | |||
| f73749ac63 | |||
| c5deba270f | |||
| bf5617393b | |||
| 057efbf98c | |||
| 2143a6614e | |||
| 6f9eb2ae75 | |||
| 73c2945961 | |||
| 18beffcc29 | |||
| 2b17319855 | |||
| d77c1ce2b4 | |||
| b43f5c9ead | |||
| a8651ec558 | |||
| d76a173706 | |||
| 7ec3cb05f2 | |||
| a83d514169 | |||
| 1f8551135f | |||
| 1444739cc6 | |||
| c7cbecad68 | |||
| ab1c597e1c | |||
| ac21c90b6f | |||
| 9ec0836d26 | |||
| ee966010e1 | |||
| 7c99621558 | |||
| cfb3eb7d90 | |||
| 64ad2fc9f4 | |||
| 7f0909c796 |
@@ -1,5 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
out
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: ['unused-imports', 'simple-import-sort'],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:react/jsx-runtime',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'@electron-toolkit/eslint-config-ts/recommended',
|
|
||||||
'@electron-toolkit/eslint-config-prettier'
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'unused-imports/no-unused-imports': 'error',
|
|
||||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
'simple-import-sort/imports': 'error',
|
|
||||||
'simple-import-sort/exports': 'error',
|
|
||||||
'react/no-is-mounted': 'off'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/.yarn/** linguist-vendored
|
||||||
|
/.yarn/releases/* binary
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: 🐛 错误报告 (中文)
|
||||||
|
description: 创建一个报告以帮助我们改进
|
||||||
|
title: '[错误]: '
|
||||||
|
labels: ['bug']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您花时间填写此错误报告!
|
||||||
|
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: 提交前检查
|
||||||
|
description: |
|
||||||
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
|
options:
|
||||||
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
|
required: true
|
||||||
|
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
||||||
|
required: true
|
||||||
|
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: 平台
|
||||||
|
description: 您正在使用哪个平台?
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: 版本
|
||||||
|
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||||
|
placeholder: 例如 v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: 错误描述
|
||||||
|
description: 描述问题时请尽可能详细
|
||||||
|
placeholder: 告诉我们发生了什么...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: 重现步骤
|
||||||
|
description: 提供详细的重现步骤,以便于我们可以准确地重现问题
|
||||||
|
placeholder: |
|
||||||
|
1. 转到 '...'
|
||||||
|
2. 点击 '....'
|
||||||
|
3. 向下滚动到 '....'
|
||||||
|
4. 看到错误
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: 预期行为
|
||||||
|
description: 清晰简洁地描述您期望发生的事情
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: 相关日志输出
|
||||||
|
description: 请复制并粘贴任何相关的日志输出
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: 附加信息
|
||||||
|
description: 任何能让我们对你所遇到的问题有更多了解的东西
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: 💡 功能建议 (中文)
|
||||||
|
description: 为项目提出新的想法
|
||||||
|
title: '[功能]: '
|
||||||
|
labels: ['enhancement']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您花时间提出新的功能建议!
|
||||||
|
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: 提交前检查
|
||||||
|
description: |
|
||||||
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
|
options:
|
||||||
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
|
required: true
|
||||||
|
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的建议。
|
||||||
|
required: true
|
||||||
|
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||||
|
required: true
|
||||||
|
- label: 最新的 Cherry Studio 版本没有实现我所提出的功能。
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: 平台
|
||||||
|
description: 您正在使用哪个平台?
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: 版本
|
||||||
|
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||||
|
placeholder: 例如 v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: 您的功能建议是否与某个问题/issue相关?
|
||||||
|
description: 请简明扼要地描述您遇到的问题
|
||||||
|
placeholder: 我总是感到沮丧,因为...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: 请描述您希望实现的解决方案
|
||||||
|
description: 请简明扼要地描述您希望发生的情况
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: 请描述您考虑过的其他方案
|
||||||
|
description: 请简明扼要地描述您考虑过的任何其他解决方案或功能
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: 其他补充信息
|
||||||
|
description: 在此添加任何其他与功能建议相关的上下文或截图
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
name: ❓ 讨论 & 提问 (中文)
|
||||||
|
description: 寻求帮助、讨论问题、提出疑问等...
|
||||||
|
title: '[讨论]: '
|
||||||
|
labels: ['question']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您的提问!请尽可能详细地描述您的问题,这样我们才能更好地帮助您。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Issue 检查清单
|
||||||
|
description: |
|
||||||
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
|
options:
|
||||||
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
|
required: true
|
||||||
|
- label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议。
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: 平台
|
||||||
|
description: 您正在使用哪个平台?
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: 版本
|
||||||
|
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||||
|
placeholder: 例如 v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: question
|
||||||
|
attributes:
|
||||||
|
label: 您的问题
|
||||||
|
description: 请详细描述您的问题
|
||||||
|
placeholder: 请尽可能清楚地说明您的问题...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: 相关背景
|
||||||
|
description: 请提供一些背景信息,帮助我们更好地理解您的问题
|
||||||
|
placeholder: 例如:使用场景、已尝试的解决方案等
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: 补充信息
|
||||||
|
description: 任何其他相关的信息、截图或代码示例
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority
|
||||||
|
attributes:
|
||||||
|
label: 优先级
|
||||||
|
description: 这个问题对您来说有多紧急?
|
||||||
|
options:
|
||||||
|
- 低 (有空再看)
|
||||||
|
- 中 (希望尽快得到答复)
|
||||||
|
- 高 (阻碍工作进行)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
name: 🐛 Bug Report (English)
|
||||||
|
description: Create a report to help us improve
|
||||||
|
title: '[Bug]: '
|
||||||
|
labels: ['bug']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for taking the time to fill out this bug report!
|
||||||
|
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Issue Checklist
|
||||||
|
description: |
|
||||||
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
|
options:
|
||||||
|
- label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
|
||||||
|
required: true
|
||||||
|
- label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found.
|
||||||
|
required: true
|
||||||
|
- label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: What platform are you using?
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: What version of Cherry Studio are you running?
|
||||||
|
placeholder: e.g. v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Bug Description
|
||||||
|
description: Please be as detailed as possible when describing the problem. Please provide screenshots or screen recordings whenever possible to help us better understand the issue.
|
||||||
|
placeholder: Tell us what happened... (Remember to attach screenshots/recordings if applicable)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Steps To Reproduce
|
||||||
|
description: Provide detailed steps to reproduce the issue so that our developers can reproduce the issue accurately. Please include screenshots or screen recordings for each step when possible.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
Remember to attach screenshots/recordings for each step when possible!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: A clear and concise description of what you expected to happen
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant Log Output
|
||||||
|
description: Please copy and paste any relevant log output
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Anything that gives us a better understanding of the problem you're experiencing
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: 💡 Feature Request (English)
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
title: '[Feature]: '
|
||||||
|
labels: ['enhancement']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for taking the time to submit a feature request!
|
||||||
|
Before submitting this issue, please make sure you have reviewed the [Project Roadmap](https://docs.cherry-ai.com/cherrystudio/planning) and the [Feature Overview](https://docs.cherry-ai.com/cherrystudio/preview).
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Issue Checklist
|
||||||
|
description: |
|
||||||
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
|
options:
|
||||||
|
- label: I understand that issues are for reporting problems and requesting features, not for off-topic comments, and I will provide as much detail as possible to help resolve the issue.
|
||||||
|
required: true
|
||||||
|
- label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion.
|
||||||
|
required: true
|
||||||
|
- label: I have provided a short and descriptive title so that developers can quickly understand the issue when browsing the issue list, rather than vague titles like "A suggestion" or "Stuck."
|
||||||
|
required: true
|
||||||
|
- label: The latest version of Cherry Studio does not include the feature I am suggesting.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: What platform are you using?
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: What version of Cherry Studio are you running?
|
||||||
|
placeholder: e.g. v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to an existing issue?
|
||||||
|
description: Please briefly describe the problem you are experiencing. If possible, include screenshots or recordings to help illustrate the current situation or pain points.
|
||||||
|
placeholder: I often feel frustrated because... (Remember to attach screenshots/recordings if applicable)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Desired Solution
|
||||||
|
description: Please briefly describe what you would like to happen. You can include mockups, screenshots, or screen recordings to better illustrate your proposed solution.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternative Solutions
|
||||||
|
description: Please briefly describe any alternative solutions or features you have considered. Feel free to include screenshots or mockups of alternative approaches.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Add any other context, screenshots, mockups or recordings that can help us better understand your feature request.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: ❓ Discussion & Questions
|
||||||
|
description: Seeking help, discussing issues, asking questions, etc...
|
||||||
|
title: '[Discussion]: '
|
||||||
|
labels: ['question']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for your question! Please describe your issue in as much detail as possible so that we can better assist you.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Issue Checklist
|
||||||
|
description: |
|
||||||
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
|
options:
|
||||||
|
- label: I understand that issues are meant for feedback and problem-solving, not for venting, and I will provide as much detail as possible to help resolve the issue.
|
||||||
|
required: true
|
||||||
|
- label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion.
|
||||||
|
required: true
|
||||||
|
- label: I confirm that I am here to ask questions and discuss issues, not to report bugs or request features.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: What platform are you using?
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: What version of Cherry Studio are you running?
|
||||||
|
placeholder: e.g. v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: question
|
||||||
|
attributes:
|
||||||
|
label: Your Question
|
||||||
|
description: Please describe your issue in detail. Include screenshots or screen recordings whenever possible to help us better understand your question.
|
||||||
|
placeholder: Please explain your issue as clearly as possible...(Remember to attach screenshots/recordings if applicable)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Context
|
||||||
|
description: Please provide some background information to help us better understand your question. Screenshots or recordings of your current setup or situation can be very helpful.
|
||||||
|
placeholder: "For example: use case, solutions you've tried, etc. Don't forget to include relevant screenshots/recordings!"
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Any other relevant information, screenshots, recordings, or code examples that can help us better assist you
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: priority
|
||||||
|
attributes:
|
||||||
|
label: Priority
|
||||||
|
description: How urgent is this issue for you?
|
||||||
|
options:
|
||||||
|
- Low (Review when available)
|
||||||
|
- Medium (Would like a response soon)
|
||||||
|
- High (Blocking progress)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
name: Nightly Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 17 * * *' # 1:00 BJ Time
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nightly-build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install corepack
|
||||||
|
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||||
|
|
||||||
|
- name: Get yarn cache directory path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache yarn dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
node_modules
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Generate date tag
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Build Linux
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
yarn build:npm linux
|
||||||
|
yarn build:linux
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||||
|
|
||||||
|
- name: Build Mac
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
yarn build:npm mac
|
||||||
|
yarn build:mac
|
||||||
|
env:
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ vars.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||||
|
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build Windows
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: yarn build:win
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||||
|
|
||||||
|
- name: Replace spaces in filenames
|
||||||
|
run: node scripts/replace-spaces.js
|
||||||
|
|
||||||
|
- name: Rename artifacts with nightly format
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p renamed-artifacts
|
||||||
|
DATE=${{ steps.date.outputs.date }}
|
||||||
|
|
||||||
|
# Windows artifacts - based on actual file naming pattern
|
||||||
|
if [ "${{ matrix.os }}" == "windows-latest" ]; then
|
||||||
|
# Setup installer
|
||||||
|
find dist -name "*setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-setup.exe \;
|
||||||
|
|
||||||
|
# Portable exe
|
||||||
|
find dist -name "*portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-portable.exe \;
|
||||||
|
|
||||||
|
# Rename blockmap files to match the new exe names
|
||||||
|
if [ -f "dist/*setup.exe.blockmap" ]; then
|
||||||
|
cp dist/*setup.exe.blockmap renamed-artifacts/cherry-studio-nightly-${DATE}-setup.exe.blockmap || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# macOS artifacts
|
||||||
|
if [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||||
|
# 处理arm64架构文件
|
||||||
|
find dist -name "*-arm64.dmg" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.dmg \;
|
||||||
|
find dist -name "*-arm64.dmg.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.dmg.blockmap \;
|
||||||
|
find dist -name "*-arm64.zip" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.zip \;
|
||||||
|
find dist -name "*-arm64.zip.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.zip.blockmap \;
|
||||||
|
|
||||||
|
# 处理x64架构文件
|
||||||
|
find dist -name "*-x64.dmg" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.dmg \;
|
||||||
|
find dist -name "*-x64.dmg.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.dmg.blockmap \;
|
||||||
|
find dist -name "*-x64.zip" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.zip \;
|
||||||
|
find dist -name "*-x64.zip.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.zip.blockmap \;
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Linux artifacts
|
||||||
|
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
|
||||||
|
find dist -name "*.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.AppImage \;
|
||||||
|
find dist -name "*.snap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.snap \;
|
||||||
|
find dist -name "*.deb" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.deb \;
|
||||||
|
find dist -name "*.rpm" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.rpm \;
|
||||||
|
find dist -name "*.tar.gz" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.tar.gz \;
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy update files
|
||||||
|
cp dist/latest*.yml renamed-artifacts/ || true
|
||||||
|
|
||||||
|
# Generate SHA256 checksums (Windows)
|
||||||
|
- name: Generate SHA256 checksums (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
cd renamed-artifacts
|
||||||
|
echo "# SHA256 checksums for Windows - $(Get-Date -Format 'yyyy-MM-dd')" > SHA256SUMS.txt
|
||||||
|
Get-ChildItem -File | Where-Object { $_.Name -ne 'SHA256SUMS.txt' } | ForEach-Object {
|
||||||
|
$file = $_.Name
|
||||||
|
$hash = (Get-FileHash -Algorithm SHA256 $file).Hash.ToLower()
|
||||||
|
Add-Content -Path SHA256SUMS.txt -Value "$hash $file"
|
||||||
|
}
|
||||||
|
cat SHA256SUMS.txt
|
||||||
|
|
||||||
|
# Generate SHA256 checksums (macOS/Linux)
|
||||||
|
- name: Generate SHA256 checksums (macOS/Linux)
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd renamed-artifacts
|
||||||
|
echo "# SHA256 checksums for ${{ runner.os }} - $(date +'%Y-%m-%d')" > SHA256SUMS.txt
|
||||||
|
if command -v shasum &>/dev/null; then
|
||||||
|
# macOS
|
||||||
|
shasum -a 256 * 2>/dev/null | grep -v SHA256SUMS.txt >> SHA256SUMS.txt || echo "No files to hash" >> SHA256SUMS.txt
|
||||||
|
else
|
||||||
|
# Linux
|
||||||
|
sha256sum * 2>/dev/null | grep -v SHA256SUMS.txt >> SHA256SUMS.txt || echo "No files to hash" >> SHA256SUMS.txt
|
||||||
|
fi
|
||||||
|
cat SHA256SUMS.txt
|
||||||
|
|
||||||
|
- name: List files to be uploaded
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "准备上传的文件:"
|
||||||
|
if [ -x "$(command -v tree)" ]; then
|
||||||
|
tree renamed-artifacts
|
||||||
|
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||||
|
dir renamed-artifacts
|
||||||
|
else
|
||||||
|
ls -la renamed-artifacts
|
||||||
|
fi
|
||||||
|
echo "总计: $(find renamed-artifacts -type f | wc -l) 个文件"
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cherry-studio-nightly-${{ steps.date.outputs.date }}-${{ matrix.os }}
|
||||||
|
path: renamed-artifacts/*
|
||||||
|
retention-days: 3 # 保留3天
|
||||||
|
compression-level: 8
|
||||||
|
|
||||||
|
Build-Summary:
|
||||||
|
needs: nightly-build
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get date tag
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: all-artifacts
|
||||||
|
merge-multiple: false
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Create summary report
|
||||||
|
run: |
|
||||||
|
echo "## ⚠️ 警告:这是每日构建版本" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "此版本为自动构建的不稳定版本,仅供测试使用。不建议在生产环境中使用。" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "安装此版本前请务必备份数据,并做好数据迁移准备。" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "构建日期:$(date +'%Y-%m-%d')" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
echo "## 📦 安装包校验和" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "请在下载后验证文件完整性。提供 SHA256 校验和。" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check each platform's artifacts and show checksums if available
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
WIN_ARTIFACT_DIR="all-artifacts/cherry-studio-nightly-${{ steps.date.outputs.date }}-windows-latest"
|
||||||
|
if [ -d "$WIN_ARTIFACT_DIR" ] && [ -f "$WIN_ARTIFACT_DIR/SHA256SUMS.txt" ]; then
|
||||||
|
echo "### Windows 安装包" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat "$WIN_ARTIFACT_DIR/SHA256SUMS.txt" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "### Windows 安装包" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "❌ Windows 构建未成功完成或未生成校验和。" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
MAC_ARTIFACT_DIR="all-artifacts/cherry-studio-nightly-${{ steps.date.outputs.date }}-macos-latest"
|
||||||
|
if [ -d "$MAC_ARTIFACT_DIR" ] && [ -f "$MAC_ARTIFACT_DIR/SHA256SUMS.txt" ]; then
|
||||||
|
echo "### macOS 安装包" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat "$MAC_ARTIFACT_DIR/SHA256SUMS.txt" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "### macOS 安装包" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "❌ macOS 构建未成功完成或未生成校验和。" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
LINUX_ARTIFACT_DIR="all-artifacts/cherry-studio-nightly-${{ steps.date.outputs.date }}-ubuntu-latest"
|
||||||
|
if [ -d "$LINUX_ARTIFACT_DIR" ] && [ -f "$LINUX_ARTIFACT_DIR/SHA256SUMS.txt" ]; then
|
||||||
|
echo "### Linux 安装包" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat "$LINUX_ARTIFACT_DIR/SHA256SUMS.txt" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "### Linux 安装包" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "❌ Linux 构建未成功完成或未生成校验和。" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "## ⚠️ Warning: This is a nightly build version" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "This version is an unstable version built automatically and is only for testing. It is not recommended to use it in a production environment." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Please backup your data before installing this version and prepare for data migration." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Build date: $(date +'%Y-%m-%d')" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Pull Request CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install corepack
|
||||||
|
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||||
|
|
||||||
|
- name: Get yarn cache directory path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache yarn dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
node_modules
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Build Check
|
||||||
|
run: yarn build:check
|
||||||
|
|
||||||
|
- name: Lint Check
|
||||||
|
run: yarn lint
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release tag (e.g. v1.0.0)'
|
||||||
|
required: true
|
||||||
|
default: 'v0.9.18'
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v*.*.*
|
- v*.*.*
|
||||||
@@ -15,62 +21,87 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get release tag
|
||||||
|
id: get-tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Install corepack
|
- name: Install corepack
|
||||||
run: corepack enable && corepack prepare yarn@4.3.1 --activate
|
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||||
|
|
||||||
|
- name: Get yarn cache directory path
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache yarn dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
node_modules
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
||||||
- name: Build Linux
|
- name: Build Linux
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: yarn build:linux
|
run: |
|
||||||
|
yarn build:npm linux
|
||||||
|
yarn build:linux
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||||
|
|
||||||
- name: Build Mac
|
- name: Build Mac
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: yarn build:mac
|
run: |
|
||||||
|
yarn build:npm mac
|
||||||
|
yarn build:mac
|
||||||
env:
|
env:
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
APPLE_ID: ${{ vars.APPLE_ID }}
|
APPLE_ID: ${{ vars.APPLE_ID }}
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }}
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: yarn build:win
|
run: yarn build:win
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||||
|
|
||||||
- name: Replace spaces in filenames
|
- name: Replace spaces in filenames
|
||||||
run: node scripts/replaceSpaces.js
|
run: node scripts/replace-spaces.js
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
files: |
|
allowUpdates: true
|
||||||
dist/*.exe
|
makeLatest: false
|
||||||
dist/*.zip
|
tag: ${{ steps.get-tag.outputs.tag }}
|
||||||
dist/*.dmg
|
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/*.blockmap'
|
||||||
dist/*.AppImage
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
dist/*.snap
|
|
||||||
dist/*.deb
|
|
||||||
dist/*.rpm
|
|
||||||
dist/*.tar.gz
|
|
||||||
dist/latest*.yml
|
|
||||||
dist/*.blockmap
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
out
|
out
|
||||||
build/icons
|
build/icons
|
||||||
|
stats.html
|
||||||
|
|
||||||
# ENV
|
# ENV
|
||||||
.env
|
.env
|
||||||
@@ -43,3 +44,6 @@ build/icons
|
|||||||
|
|
||||||
# Local
|
# Local
|
||||||
local
|
local
|
||||||
|
.aider*
|
||||||
|
.cursorrules
|
||||||
|
.cursor/rules
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
yarn lint-staged
|
||||||
@@ -5,3 +5,4 @@ LICENSE.md
|
|||||||
tsconfig.json
|
tsconfig.json
|
||||||
tsconfig.*.json
|
tsconfig.*.json
|
||||||
CHANGELOG*.md
|
CHANGELOG*.md
|
||||||
|
agents.json
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/dist/**": true
|
"**/dist/**": true,
|
||||||
|
".yarn/releases/**": true
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
diff --git a/lib/check-signature.js b/lib/check-signature.js
|
|
||||||
index 324568af71bcc4372c9f959131ecd24122848c86..677348e0a138ff608b2ac41f592d813b15ee4956 100644
|
|
||||||
--- a/lib/check-signature.js
|
|
||||||
+++ b/lib/check-signature.js
|
|
||||||
@@ -41,16 +41,12 @@ const spawn_1 = require("./spawn");
|
|
||||||
const debug_1 = __importDefault(require("debug"));
|
|
||||||
const d = (0, debug_1.default)('electron-notarize');
|
|
||||||
const codesignDisplay = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
||||||
- const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', path.basename(opts.appPath)], {
|
|
||||||
- cwd: path.dirname(opts.appPath),
|
|
||||||
- });
|
|
||||||
+ const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', opts.appPath]);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
const codesign = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
||||||
d('attempting to check codesign of app:', opts.appPath);
|
|
||||||
- const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', path.basename(opts.appPath)], {
|
|
||||||
- cwd: path.dirname(opts.appPath),
|
|
||||||
- });
|
|
||||||
+ const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', opts.appPath]);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
function checkSignatures(opts) {
|
|
||||||
diff --git a/lib/notarytool.js b/lib/notarytool.js
|
|
||||||
index 1ab090efb2101fc8bee5553445e0349c54474421..a5ddfd922197449fc56078e4a7e9a2ee5d8d207d 100644
|
|
||||||
--- a/lib/notarytool.js
|
|
||||||
+++ b/lib/notarytool.js
|
|
||||||
@@ -92,9 +92,7 @@ function notarizeAndWaitForNotaryTool(opts) {
|
|
||||||
else {
|
|
||||||
filePath = path.resolve(dir, `${path.parse(opts.appPath).name}.zip`);
|
|
||||||
d('zipping application to:', filePath);
|
|
||||||
- const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), filePath], {
|
|
||||||
- cwd: path.dirname(opts.appPath),
|
|
||||||
- });
|
|
||||||
+ const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', opts.appPath, filePath]);
|
|
||||||
if (zipResult.code !== 0) {
|
|
||||||
throw new Error(`Failed to zip application, exited with code: ${zipResult.code}\n\n${zipResult.output}`);
|
|
||||||
}
|
|
||||||
diff --git a/lib/staple.js b/lib/staple.js
|
|
||||||
index 47dbd85b2fc279d999b57f47fb8171e1cc674436..f8829e6ac54fcd630a730d12d75acc1591b953b6 100644
|
|
||||||
--- a/lib/staple.js
|
|
||||||
+++ b/lib/staple.js
|
|
||||||
@@ -43,9 +43,7 @@ const d = (0, debug_1.default)('electron-notarize:staple');
|
|
||||||
function stapleApp(opts) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
d('attempting to staple app:', opts.appPath);
|
|
||||||
- const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', path.basename(opts.appPath)], {
|
|
||||||
- cwd: path.dirname(opts.appPath),
|
|
||||||
- });
|
|
||||||
+ const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', opts.appPath]);
|
|
||||||
if (result.code !== 0) {
|
|
||||||
throw new Error(`Failed to staple your application with code: ${result.code}\n\n${result.output}`);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
diff --git a/dist/embeddings.js b/dist/embeddings.js
|
||||||
|
index 1f8154be3e9c22442a915eb4b85fa6d2a21b0d0c..dc13ef4a30e6c282824a5357bcee9bd0ae222aab 100644
|
||||||
|
--- a/dist/embeddings.js
|
||||||
|
+++ b/dist/embeddings.js
|
||||||
|
@@ -214,10 +214,12 @@ export class OpenAIEmbeddings extends Embeddings {
|
||||||
|
* @returns Promise that resolves to an embedding for the document.
|
||||||
|
*/
|
||||||
|
async embedQuery(text) {
|
||||||
|
+ const isBaiduCloud = this.clientConfig.baseURL.includes('baidubce.com')
|
||||||
|
+ const input = this.stripNewLines ? text.replace(/\n/g, ' ') : text
|
||||||
|
const params = {
|
||||||
|
model: this.model,
|
||||||
|
- input: this.stripNewLines ? text.replace(/\n/g, " ") : text,
|
||||||
|
- };
|
||||||
|
+ input: isBaiduCloud ? [input] : input
|
||||||
|
+ }
|
||||||
|
if (this.dimensions) {
|
||||||
|
params.dimensions = this.dimensions;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
|
||||||
|
index eaf30b114a273e68abbb92c8b07018495e63f4cb..4b06519bdb51845e4693fe877da9de01c7a81039 100644
|
||||||
|
--- a/src/markdown-loader.js
|
||||||
|
+++ b/src/markdown-loader.js
|
||||||
|
@@ -21,7 +21,7 @@ export class MarkdownLoader extends BaseLoader {
|
||||||
|
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
|
||||||
|
: await streamToBuffer(fs.createReadStream(this.filePathOrUrl));
|
||||||
|
this.debug('MarkdownLoader stream created');
|
||||||
|
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
|
||||||
|
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
|
||||||
|
this.debug('Markdown parsed...');
|
||||||
|
const webLoader = new WebLoader({
|
||||||
|
urlOrContent: result,
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
diff --git a/src/loaders/local-path-loader.d.ts b/src/loaders/local-path-loader.d.ts
|
||||||
|
index 48c20e68c469cd309be2dc8f28e44c1bd04a26e9..1c16d83bcbf9b7140292793d6cbb8c04281949d9 100644
|
||||||
|
--- a/src/loaders/local-path-loader.d.ts
|
||||||
|
+++ b/src/loaders/local-path-loader.d.ts
|
||||||
|
@@ -4,8 +4,10 @@ export declare class LocalPathLoader extends BaseLoader<{
|
||||||
|
}> {
|
||||||
|
private readonly debug;
|
||||||
|
private readonly path;
|
||||||
|
- constructor({ path }: {
|
||||||
|
+ constructor({ path, chunkSize, chunkOverlap }: {
|
||||||
|
path: string;
|
||||||
|
+ chunkSize?: number;
|
||||||
|
+ chunkOverlap?: number;
|
||||||
|
});
|
||||||
|
getUnfilteredChunks(): AsyncGenerator<{
|
||||||
|
metadata: {
|
||||||
|
diff --git a/src/loaders/local-path-loader.js b/src/loaders/local-path-loader.js
|
||||||
|
index 4cf8a6bd1d890244c8ec49d4a05ee3bd58861c79..ec8215b01195a21ef20f3c5d56ecc99f186bb596 100644
|
||||||
|
--- a/src/loaders/local-path-loader.js
|
||||||
|
+++ b/src/loaders/local-path-loader.js
|
||||||
|
@@ -8,8 +8,8 @@ import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||||
|
export class LocalPathLoader extends BaseLoader {
|
||||||
|
debug = createDebugMessages('embedjs:loader:LocalPathLoader');
|
||||||
|
path;
|
||||||
|
- constructor({ path }) {
|
||||||
|
- super(`LocalPathLoader_${md5(path)}`, { path });
|
||||||
|
+ constructor({ path, chunkSize, chunkOverlap }) {
|
||||||
|
+ super(`LocalPathLoader_${md5(path)}`, { path }, chunkSize ?? 1000, chunkOverlap ?? 0);
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
async *getUnfilteredChunks() {
|
||||||
|
@@ -36,10 +36,12 @@ export class LocalPathLoader extends BaseLoader {
|
||||||
|
const extension = currentPath.split('.').pop().toLowerCase();
|
||||||
|
if (extension === 'md' || extension === 'mdx')
|
||||||
|
mime = 'text/markdown';
|
||||||
|
+ if (extension === 'txt')
|
||||||
|
+ mime = 'text/plain';
|
||||||
|
this.debug(`File '${this.path}' mime type updated to 'text/markdown'`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
- const loader = await createLoaderFromMimeType(currentPath, mime);
|
||||||
|
+ const loader = await createLoaderFromMimeType(currentPath, mime, this.chunkSize, this.chunkOverlap);
|
||||||
|
for await (const result of await loader.getUnfilteredChunks()) {
|
||||||
|
yield {
|
||||||
|
pageContent: result.pageContent,
|
||||||
|
diff --git a/src/util/mime.d.ts b/src/util/mime.d.ts
|
||||||
|
index 57f56a1b8edc98366af9f84d671676c41c2f01ca..14be3b5727cff6eb1978838045e9a788f8f53bfb 100644
|
||||||
|
--- a/src/util/mime.d.ts
|
||||||
|
+++ b/src/util/mime.d.ts
|
||||||
|
@@ -1,2 +1,2 @@
|
||||||
|
import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||||
|
-export declare function createLoaderFromMimeType(loaderData: string, mimeType: string): Promise<BaseLoader>;
|
||||||
|
+export declare function createLoaderFromMimeType(loaderData: string, mimeType: string, chunkSize?: number, chunkOverlap?: number): Promise<BaseLoader>;
|
||||||
|
diff --git a/src/util/mime.js b/src/util/mime.js
|
||||||
|
index b6426a859968e2bf6206795f70333e90ae27aeb7..16ae2adb863f8d7abfa757f1c5cc39f6bb1c44fa 100644
|
||||||
|
--- a/src/util/mime.js
|
||||||
|
+++ b/src/util/mime.js
|
||||||
|
@@ -1,7 +1,9 @@
|
||||||
|
import mime from 'mime';
|
||||||
|
import createDebugMessages from 'debug';
|
||||||
|
import { TextLoader } from '../loaders/text-loader.js';
|
||||||
|
-export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||||
|
+import fs from 'node:fs'
|
||||||
|
+
|
||||||
|
+export async function createLoaderFromMimeType(loaderData, mimeType, chunkSize, chunkOverlap) {
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')(`Incoming mime type '${mimeType}'`);
|
||||||
|
switch (mimeType) {
|
||||||
|
case 'application/msword':
|
||||||
|
@@ -10,7 +12,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load docx files');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported DocxLoader');
|
||||||
|
- return new DocxLoader({ filePathOrUrl: loaderData });
|
||||||
|
+ return new DocxLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'application/vnd.ms-excel':
|
||||||
|
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
|
||||||
|
@@ -18,21 +20,21 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load excel files');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported ExcelLoader');
|
||||||
|
- return new ExcelLoader({ filePathOrUrl: loaderData });
|
||||||
|
+ return new ExcelLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'application/pdf': {
|
||||||
|
const { PdfLoader } = await import('@llm-tools/embedjs-loader-pdf').catch(() => {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-pdf` needs to be installed to load PDF files');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PdfLoader');
|
||||||
|
- return new PdfLoader({ filePathOrUrl: loaderData });
|
||||||
|
+ return new PdfLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': {
|
||||||
|
const { PptLoader } = await import('@llm-tools/embedjs-loader-msoffice').catch(() => {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load pptx files');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PptLoader');
|
||||||
|
- return new PptLoader({ filePathOrUrl: loaderData });
|
||||||
|
+ return new PptLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'text/plain': {
|
||||||
|
const fineType = mime.getType(loaderData);
|
||||||
|
@@ -42,24 +44,24 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
|
||||||
|
- return new CsvLoader({ filePathOrUrl: loaderData });
|
||||||
|
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
- else
|
||||||
|
- return new TextLoader({ text: loaderData });
|
||||||
|
+ const content = fs.readFileSync(loaderData, 'utf-8');
|
||||||
|
+ return new TextLoader({ text: content, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'application/csv': {
|
||||||
|
const { CsvLoader } = await import('@llm-tools/embedjs-loader-csv').catch(() => {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
|
||||||
|
- return new CsvLoader({ filePathOrUrl: loaderData });
|
||||||
|
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'text/html': {
|
||||||
|
const { WebLoader } = await import('@llm-tools/embedjs-loader-web').catch(() => {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-web` needs to be installed to load web documents');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported WebLoader');
|
||||||
|
- return new WebLoader({ urlOrContent: loaderData });
|
||||||
|
+ return new WebLoader({ urlOrContent: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'text/xml': {
|
||||||
|
const { SitemapLoader } = await import('@llm-tools/embedjs-loader-sitemap').catch(() => {
|
||||||
|
@@ -67,14 +69,14 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported SitemapLoader');
|
||||||
|
if (await SitemapLoader.test(loaderData)) {
|
||||||
|
- return new SitemapLoader({ url: loaderData });
|
||||||
|
+ return new SitemapLoader({ url: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
//This is not a Sitemap but is still XML
|
||||||
|
const { XmlLoader } = await import('@llm-tools/embedjs-loader-xml').catch(() => {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-xml` needs to be installed to load XML documents');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported XmlLoader');
|
||||||
|
- return new XmlLoader({ filePathOrUrl: loaderData });
|
||||||
|
+ return new XmlLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'text/x-markdown':
|
||||||
|
case 'text/markdown': {
|
||||||
|
@@ -82,7 +84,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||||
|
throw new Error('Package `@llm-tools/embedjs-loader-markdown` needs to be installed to load markdown files');
|
||||||
|
});
|
||||||
|
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported MarkdownLoader');
|
||||||
|
- return new MarkdownLoader({ filePathOrUrl: loaderData });
|
||||||
|
+ return new MarkdownLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||||
|
}
|
||||||
|
case 'image/png':
|
||||||
|
case 'image/jpeg': {
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
diff --git a/dist/cjs/client/stdio.js b/dist/cjs/client/stdio.js
|
||||||
|
index 2ada8771c5f76673b5021d6453c6bdd7e0b88013..89f6ea9ca7de86294d3d966f3454b98e19bfe534 100644
|
||||||
|
--- a/dist/cjs/client/stdio.js
|
||||||
|
+++ b/dist/cjs/client/stdio.js
|
||||||
|
@@ -68,7 +68,7 @@ class StdioClientTransport {
|
||||||
|
this._process = (0, node_child_process_1.spawn)(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
|
||||||
|
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
|
||||||
|
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
|
||||||
|
- shell: false,
|
||||||
|
+ shell: process.platform === 'win32' ? true : false,
|
||||||
|
signal: this._abortController.signal,
|
||||||
|
windowsHide: node_process_1.default.platform === "win32" && isElectron(),
|
||||||
|
cwd: this._serverParams.cwd,
|
||||||
|
diff --git a/dist/esm/client/stdio.js b/dist/esm/client/stdio.js
|
||||||
|
index 387c982fd40fd8db9790a78e1a05c9ecb81501c0..7b7e60a306bca73149609015a27e904a0a68ca02 100644
|
||||||
|
--- a/dist/esm/client/stdio.js
|
||||||
|
+++ b/dist/esm/client/stdio.js
|
||||||
|
@@ -61,7 +61,7 @@ export class StdioClientTransport {
|
||||||
|
this._process = spawn(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
|
||||||
|
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
|
||||||
|
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
|
||||||
|
- shell: false,
|
||||||
|
+ shell: process.platform === 'win32' ? true : false,
|
||||||
|
signal: this._abortController.signal,
|
||||||
|
windowsHide: process.platform === "win32" && isElectron(),
|
||||||
|
cwd: this._serverParams.cwd,
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
diff --git a/dist/index.js b/dist/index.js
|
||||||
|
index 88c405a000d21b3631eaa378690907c5527b8eaf..e03e66440c7c93aee38adf57df3096c6fefcd96d 100644
|
||||||
|
--- a/dist/index.js
|
||||||
|
+++ b/dist/index.js
|
||||||
|
@@ -82,7 +82,6 @@ module.exports = __toCommonJS(index_exports);
|
||||||
|
|
||||||
|
// src/utils.ts
|
||||||
|
var import_axios = __toESM(require("axios"));
|
||||||
|
-var import_js_tiktoken = require("js-tiktoken");
|
||||||
|
var BASE_URL = "https://api.tavily.com";
|
||||||
|
var DEFAULT_MODEL_ENCODING = "gpt-3.5-turbo";
|
||||||
|
var DEFAULT_MAX_TOKENS = 4e3;
|
||||||
|
@@ -97,8 +96,7 @@ function post(endpoint, body, apiKey) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getTotalTokensFromString(str, encodingName = DEFAULT_MODEL_ENCODING) {
|
||||||
|
- const encoding = (0, import_js_tiktoken.encodingForModel)(encodingName);
|
||||||
|
- return encoding.encode(str).length;
|
||||||
|
+ return 0;
|
||||||
|
}
|
||||||
|
function getMaxTokensFromList(data, maxTokens = DEFAULT_MAX_TOKENS) {
|
||||||
|
var result = [];
|
||||||
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
|
index 0a9ea6a0add8d709e6721e806571f373d9fe0487..b81f1ea48a2b2a30ee98d53980a1b04ea3fdc5d4 100644
|
||||||
|
--- a/dist/index.mjs
|
||||||
|
+++ b/dist/index.mjs
|
||||||
|
@@ -49,7 +49,6 @@ var __async = (__this, __arguments, generator) => {
|
||||||
|
|
||||||
|
// src/utils.ts
|
||||||
|
import axios from "axios";
|
||||||
|
-import { encodingForModel } from "js-tiktoken";
|
||||||
|
var BASE_URL = "https://api.tavily.com";
|
||||||
|
var DEFAULT_MODEL_ENCODING = "gpt-3.5-turbo";
|
||||||
|
var DEFAULT_MAX_TOKENS = 4e3;
|
||||||
|
@@ -64,8 +63,7 @@ function post(endpoint, body, apiKey) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getTotalTokensFromString(str, encodingName = DEFAULT_MODEL_ENCODING) {
|
||||||
|
- const encoding = encodingForModel(encodingName);
|
||||||
|
- return encoding.encode(str).length;
|
||||||
|
+ return 0;
|
||||||
|
}
|
||||||
|
function getMaxTokensFromList(data, maxTokens = DEFAULT_MAX_TOKENS) {
|
||||||
|
var result = [];
|
||||||
|
diff --git a/package.json b/package.json
|
||||||
|
index 36d4a613166a7906c1dc5377a89dc0a65f746f73..dc6e0e9363046755cad123e627cc270a2e3580d1 100644
|
||||||
|
--- a/package.json
|
||||||
|
+++ b/package.json
|
||||||
|
@@ -36,7 +36,6 @@
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
- "axios": "^1.7.7",
|
||||||
|
- "js-tiktoken": "^1.0.14"
|
||||||
|
+ "axios": "^1.7.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
diff --git a/epub.js b/epub.js
|
||||||
|
index 50efff7678ca4879ed639d3bb70fd37e7477fd16..accbe689cd200bd59475dd20fca596511d0f33e0 100644
|
||||||
|
--- a/epub.js
|
||||||
|
+++ b/epub.js
|
||||||
|
@@ -3,9 +3,28 @@ var xml2jsOptions = xml2js.defaults['0.1'];
|
||||||
|
var EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
|
try {
|
||||||
|
- // zipfile is an optional dependency:
|
||||||
|
- var ZipFile = require("zipfile").ZipFile;
|
||||||
|
-} catch (err) {
|
||||||
|
+ var zipread = require("zipread");
|
||||||
|
+ var ZipFile = function(filename) {
|
||||||
|
+ var zip = zipread(filename);
|
||||||
|
+ this.zip = zip;
|
||||||
|
+ var files = zip.files;
|
||||||
|
+
|
||||||
|
+ files = Object.values(files).filter((file) => {
|
||||||
|
+ return !file.dir;
|
||||||
|
+ }).map((file) => {
|
||||||
|
+ return file.name;
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ this.names = files;
|
||||||
|
+ this.count = this.names.length;
|
||||||
|
+ };
|
||||||
|
+ ZipFile.prototype.readFile = function(name, cb) {
|
||||||
|
+ this.zip.readFile(name
|
||||||
|
+ , function(err, buffer) {
|
||||||
|
+ return cb(null, buffer);
|
||||||
|
+ });
|
||||||
|
+ };
|
||||||
|
+} catch(err) {
|
||||||
|
// Mock zipfile using pure-JS adm-zip:
|
||||||
|
var AdmZip = require('adm-zip');
|
||||||
|
|
||||||
|
diff --git a/package.json b/package.json
|
||||||
|
index 8c3dccf0caac8913a2edabd7049b25bb9063c905..57bac3b71ddd73916adbdf00b049089181db5bcb 100644
|
||||||
|
--- a/package.json
|
||||||
|
+++ b/package.json
|
||||||
|
@@ -40,10 +40,8 @@
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"adm-zip": "^0.4.11",
|
||||||
|
- "xml2js": "^0.4.23"
|
||||||
|
- },
|
||||||
|
- "optionalDependencies": {
|
||||||
|
- "zipfile": "^0.5.11"
|
||||||
|
+ "xml2js": "^0.4.23",
|
||||||
|
+ "zipread": "^1.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/mocha": "^5.2.5",
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
diff --git a/core.js b/core.js
|
||||||
|
index e75a18281ce8f051990c5a50bc1076afdddf91a3..e62f796791a155f23d054e74a429516c14d6e11b 100644
|
||||||
|
--- a/core.js
|
||||||
|
+++ b/core.js
|
||||||
|
@@ -156,7 +156,7 @@ class APIClient {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': this.getUserAgent(),
|
||||||
|
- ...getPlatformHeaders(),
|
||||||
|
+ // ...getPlatformHeaders(),
|
||||||
|
...this.authHeaders(opts),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
diff --git a/core.mjs b/core.mjs
|
||||||
|
index fcef58eb502664c41a77483a00db8adaf29b2817..18c5d6ed4be86b3640931277bdc27700006764d7 100644
|
||||||
|
--- a/core.mjs
|
||||||
|
+++ b/core.mjs
|
||||||
|
@@ -149,7 +149,7 @@ export class APIClient {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': this.getUserAgent(),
|
||||||
|
- ...getPlatformHeaders(),
|
||||||
|
+ // ...getPlatformHeaders(),
|
||||||
|
...this.authHeaders(opts),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
diff --git a/error.mjs b/error.mjs
|
||||||
|
index 7d19f5578040afa004bc887aab1725e8703d2bac..59ec725b6142299a62798ac4bdedb63ba7d9932c 100644
|
||||||
|
--- a/error.mjs
|
||||||
|
+++ b/error.mjs
|
||||||
|
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
|
||||||
|
if (!status || !headers) {
|
||||||
|
return new APIConnectionError({ message, cause: castToError(errorResponse) });
|
||||||
|
}
|
||||||
|
- const error = errorResponse?.['error'];
|
||||||
|
+ const error = errorResponse?.['error'] || errorResponse;
|
||||||
|
if (status === 400) {
|
||||||
|
return new BadRequestError(status, error, message, headers);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
diff --git a/lib/pdf-parse.js b/lib/pdf-parse.js
|
||||||
|
index 96bfbc705dcb4fb73cb077a75f02c115371b3477..6d02d2bb426063c3a31cb740c3d86841de162a22 100644
|
||||||
|
--- a/lib/pdf-parse.js
|
||||||
|
+++ b/lib/pdf-parse.js
|
||||||
|
@@ -21,12 +21,12 @@ function render_page(pageData) {
|
||||||
|
for (let item of textContent.items) {
|
||||||
|
if (lastY == item.transform[5] || !lastY){
|
||||||
|
text += item.str;
|
||||||
|
- }
|
||||||
|
+ }
|
||||||
|
else{
|
||||||
|
text += '\n' + item.str;
|
||||||
|
- }
|
||||||
|
+ }
|
||||||
|
lastY = item.transform[5];
|
||||||
|
- }
|
||||||
|
+ }
|
||||||
|
//let strings = textContent.items.map(item => item.str);
|
||||||
|
//let text = strings.join("\n");
|
||||||
|
//text = text.replace(/[ ]+/ig," ");
|
||||||
|
@@ -60,7 +60,7 @@ async function PDF(dataBuffer, options) {
|
||||||
|
if (typeof options.version != 'string') options.version = DEFAULT_OPTIONS.version;
|
||||||
|
if (options.version == 'default') options.version = DEFAULT_OPTIONS.version;
|
||||||
|
|
||||||
|
- PDFJS = PDFJS ? PDFJS : require(`./pdf.js/${options.version}/build/pdf.js`);
|
||||||
|
+ PDFJS = PDFJS ? PDFJS : require(`./pdf.js/v1.10.100/build/pdf.js`);
|
||||||
|
|
||||||
|
ret.version = PDFJS.version;
|
||||||
|
|
||||||
@@ -3,3 +3,5 @@ enableImmutableInstalls: false
|
|||||||
httpTimeout: 300000
|
httpTimeout: 300000
|
||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
yarnPath: .yarn/releases/yarn-4.6.0.cjs
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
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:
|
# Code of Conduct
|
||||||
|
|
||||||
**I. Commercial Use License**
|
## Our Pledge
|
||||||
|
|
||||||
1. **Free Commercial Use**: Users can use the software for commercial purposes without modifying the code.
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
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**
|
## Our Standards
|
||||||
|
|
||||||
As a contributor to Cherry Studio, you agree to the following:
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
|
|
||||||
1. **License Adjustment**: The producer reserves the right to adjust the open-source license as needed, making it stricter or more lenient.
|
- Using welcoming and inclusive language
|
||||||
2. **Commercial Use**: Any code you contribute may be used for commercial purposes, including but not limited to cloud business operations.
|
- Being respectful of differing viewpoints and experiences
|
||||||
|
- Gracefully accepting constructive criticism
|
||||||
|
- Focusing on what is best for the community
|
||||||
|
- Showing empathy towards other community members
|
||||||
|
|
||||||
**III. Other Terms**
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
1. The interpretation of these terms is subject to the discretion of Cherry Studio developers.
|
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
2. These terms may be updated, and users will be notified through the software when changes occur.
|
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
For any questions or to request a commercial license, please contact the Cherry Studio development team.
|
## Our Responsibilities
|
||||||
|
|
||||||
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.
|
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
---
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
@@ -1,72 +1,45 @@
|
|||||||
## Cherry Studio目录结构和功能
|
# Cherry Studio 贡献者指南
|
||||||
|
|
||||||
### 1. `/src`: 主要源代码目录
|
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
|
||||||
- ** `/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`: 构建和开发脚本
|
1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。
|
||||||
- 包含npm脚本,用于开发、构建和部署
|
|
||||||
|
|
||||||
### 5. `/types`: TypeScript类型定义
|
2. **修复 BUG**:如果您发现了 BUG,欢迎提交修复方案。请在提交前确认问题已被解决,并附上相关测试。
|
||||||
- 包含自定义的类型定义文件
|
|
||||||
|
|
||||||
### 6. `/tests`: 测试文件目录
|
3. **维护 Issue**:协助我们管理 GitHub 上的 issue,帮助标记、分类和解决问题。
|
||||||
- 包含单元测试和集成测试
|
|
||||||
|
|
||||||
### 7. `/docs`: 文档目录
|
4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。
|
||||||
- 包含项目文档、API文档等
|
|
||||||
|
|
||||||
### 8. `/config`: 配置文件目录
|
5. **编写文档**:帮助我们完善用户手册、API 文档和开发者指南。
|
||||||
- 包含各种配置文件,如webpack配置、环境变量等
|
|
||||||
|
|
||||||
### 9. `/migrations`: 数据库迁移文件
|
6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。
|
||||||
- 由于使用了Sequelize,这里可能包含数据库结构的变更记录
|
|
||||||
|
|
||||||
### 10. `/models`: 数据模型
|
7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio,吸引更多用户和开发者。
|
||||||
- 定义Sequelize的数据模型,对应数据库表结构
|
|
||||||
|
|
||||||
## 主要功能实现
|
## 开始贡献
|
||||||
|
|
||||||
### 1. LLM提供商集成
|
1. **Fork 仓库**:在 GitHub 上 fork 我们的仓库,并将其克隆到本地。
|
||||||
- 可能在`/src/utils`或`/src/services`中实现与不同LLM API的集成
|
|
||||||
|
|
||||||
### 2. 多助手和多主题支持
|
2. **创建分支**:为您要进行的更改创建一个新的分支。
|
||||||
- 在`/src/store`中管理助手和主题的状态
|
|
||||||
- 在`/src/components`中实现相关的UI组件
|
|
||||||
|
|
||||||
### 3. 多模型对话
|
3. **提交更改**:在本地进行更改并提交。请确保您的提交信息清晰明了。
|
||||||
- 在`/src/pages`的聊天界面中实现
|
|
||||||
- 可能使用`/src/store`来管理对话状态
|
|
||||||
|
|
||||||
### 4. 拖放排序
|
4. **发起 Pull Request**:将您的更改推送到 GitHub,并发起 Pull Request。请描述您的更改内容和原因。
|
||||||
- 在`/src/components`中实现相关的可拖拽组件
|
|
||||||
|
|
||||||
### 5. 代码高亮
|
### 其他建议
|
||||||
- 可能使用第三方库,如Prism.js,集成在`/src/components`中
|
|
||||||
|
|
||||||
### 6. Mermaid图表支持
|
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
|
||||||
- 在`/src/components`中集成Mermaid库
|
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。
|
||||||
|
|
||||||
### 7. 数据持久化
|
## 联系我们
|
||||||
- 使用Sequelize在`/models`中定义数据模型
|
|
||||||
- 在`/migrations`中管理数据库结构变更
|
如果您有任何问题或建议,欢迎通过以下方式联系我们:
|
||||||
|
|
||||||
|
- 微信:kangfenmao
|
||||||
|
- [GitHub Issues](https://github.com/CherryHQ/cherry-studio/issues)
|
||||||
|
|
||||||
|
感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
## Cherry Studio 用户协议
|
|
||||||
|
|
||||||
欢迎使用 Cherry Studio 桌面 AI 客户端工具。请仔细阅读以下协议条款,继续使用本软件即表示您同意本协议内容。
|
|
||||||
|
|
||||||
**许可协议**
|
**许可协议**
|
||||||
|
|
||||||
本软件采用 Apache License 2.0 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry Studio 时还应遵守以下附加条款:
|
本软件采用 Apache License 2.0 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry Studio 时还应遵守以下附加条款:
|
||||||
|
|
||||||
**一. 商用许可**
|
**一. 商用许可**
|
||||||
|
|
||||||
1. **免费商用**:用户在不修改代码的情况下,可以免费用于商业目的。
|
在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:
|
||||||
2. **商业授权**:如果您满足以下任意条件之一,需取得商业授权:
|
|
||||||
1. 对本软件进行二次修改、开发(包括但不限于修改应用名称、logo、代码以及功能)。
|
1. **修改与衍生**: 您对 Cherry Studio 材料进行修改或基于其进行衍生开发(包括但不限于修改应用名称、Logo、代码、功能、界面,数据等)。
|
||||||
2. 为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。
|
2. **企业服务**: 在您的企业内部,或为企业客户提供基于 CherryStudio 的服务,且该服务支持 10 人及以上累计用户使用。
|
||||||
3. 预装或集成到硬件设备或产品中进行捆绑销售。
|
3. **硬件捆绑销售**: 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。
|
||||||
4. 政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
|
4. **政府或教育机构大规模采购**: 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
|
||||||
|
5. **面向公众的公有云服务**:基于 Cherry Studio,提供面向公众的公有云服务。
|
||||||
|
|
||||||
**二. 贡献者协议**
|
**二. 贡献者协议**
|
||||||
|
|
||||||
@@ -33,47 +30,33 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
根据 Apache 许可证 2.0 版(“许可证”)进行许可;除非符合许可证,否则您不得使用此文件。您可以在以下网址获取许可证副本:
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
除非适用法律要求或书面同意,软件根据许可证分发的内容以“原样”分发,不附带任何明示或暗示的保证或条件。请参阅特定语言管理权限的许可证和许可证下的限制。
|
|
||||||
|
|
||||||
## Cherry Studio User Agreement
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
**License Agreement**
|
**License Agreement**
|
||||||
|
|
||||||
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:
|
This software is licensed under the Apache License 2.0. In addition to the terms stipulated by the Apache License 2.0, you must comply with the following supplementary terms when using Cherry Studio:
|
||||||
|
|
||||||
**I. Commercial Use License**
|
**I. Commercial Licensing**
|
||||||
|
|
||||||
1. **Free Commercial Use**: Users can use the software for commercial purposes without modifying the code.
|
You must contact us and obtain explicit written commercial authorization to continue using Cherry Studio materials under any of the following circumstances:
|
||||||
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.
|
1. **Modifications and Derivatives:** You modify Cherry Studio materials or perform derivative development based on them (including but not limited to changing the application’s name, logo, code, functionality, user interface, data, etc.).
|
||||||
2. You provide multi-tenant services to enterprise customers with 10 or more users.
|
2. **Enterprise Services:** You use Cherry Studio internally within your enterprise, or you provide Cherry Studio-based services for enterprise customers, and such services support cumulative usage by 10 or more users.
|
||||||
3. You pre-install or integrate the software into hardware devices or products and bundle it for sale.
|
3. **Hardware Bundling and Sales:** You pre-install or integrate Cherry Studio into hardware devices or products for bundled sale.
|
||||||
4. You are engaging in large-scale procurement for government or educational institutions, especially involving security, data privacy, or other sensitive requirements.
|
4. **Large-scale Procurement by Government or Educational Institutions:** Your usage scenario involves large-scale procurement projects by government or educational institutions, especially in cases involving sensitive requirements such as security and data privacy.
|
||||||
|
5. **Public Cloud Services:** You provide public cloud-based product services utilizing Cherry Studio.
|
||||||
|
|
||||||
**II. Contributor Agreement**
|
**II. Contributor Agreement**
|
||||||
|
|
||||||
As a contributor to Cherry Studio, you agree to the following:
|
As a contributor to Cherry Studio, you must agree to the following terms:
|
||||||
|
|
||||||
1. **License Adjustment**: The producer reserves the right to adjust the open-source license as needed, making it stricter or more lenient.
|
1. **License Adjustments:** The producer reserves the right to adjust the open-source license as necessary, making it more strict or permissive.
|
||||||
2. **Commercial Use**: Any code you contribute may be used for commercial purposes, including but not limited to cloud business operations.
|
2. **Commercial Usage:** Your contributed code may be used commercially, including but not limited to cloud business operations.
|
||||||
|
|
||||||
**III. Other Terms**
|
**III. Other Terms**
|
||||||
|
|
||||||
1. The interpretation of these terms is subject to the discretion of Cherry Studio developers.
|
1. Cherry Studio developers reserve the right of final interpretation of these agreement terms.
|
||||||
2. These terms may be updated, and users will be notified through the software when changes occur.
|
2. This agreement may be updated according to practical circumstances, and users will be notified of updates through this software.
|
||||||
|
|
||||||
For any questions or to request a commercial license, please contact the Cherry Studio development team.
|
If you have any questions or need to apply for commercial authorization, 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.
|
Other than these specific conditions, all remaining rights and restrictions follow the Apache License 2.0. For more detailed information regarding Apache License 2.0, please visit 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.
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<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)
|
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
<div align="center">
|
<h1 align="center">
|
||||||
<a href="https://github.com/kangfenmao/cherry-studio/releases">
|
<a href="https://github.com/CherryHQ/cherry-studio/releases">
|
||||||
<img src="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</h1>
|
||||||
|
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a><br></p>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
English | <a href="./README.zh.md">中文</a> | <a href="./README.ja.md">日本語</a>
|
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# 🍒 Cherry Studio
|
# 🍒 Cherry Studio
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
||||||
|
|
||||||
👏 Join [Telegram Group](https://t.me/CherryStudioAI)
|
👏 Join [Telegram Group](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ Group(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
|
||||||
|
|
||||||
|
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
|
||||||
|
|
||||||
# 🌠 Screenshot
|
# 🌠 Screenshot
|
||||||
|
|
||||||
@@ -21,71 +22,117 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
# 🌟 Features
|
# 🌟 Key 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.
|
1. **Diverse LLM Provider Support**:
|
||||||
2. Allows creation of multiple Assistants.
|
|
||||||
3. Enables creation of multiple topics.
|
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
||||||
4. Allows using multiple models to answer questions in the same conversation.
|
- 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others
|
||||||
5. Supports drag-and-drop sorting.
|
- 💻 Local Model Support with Ollama, LM Studio
|
||||||
6. Code highlighting.
|
|
||||||
7. Mermaid chart
|
2. **AI Assistants & Conversations**:
|
||||||
|
|
||||||
|
- 📚 300+ Pre-configured AI Assistants
|
||||||
|
- 🤖 Custom Assistant Creation
|
||||||
|
- 💬 Multi-model Simultaneous Conversations
|
||||||
|
|
||||||
|
3. **Document & Data Processing**:
|
||||||
|
|
||||||
|
- 📄 Support for Text, Images, Office, PDF, and more
|
||||||
|
- ☁️ WebDAV File Management and Backup
|
||||||
|
- 📊 Mermaid Chart Visualization
|
||||||
|
- 💻 Code Syntax Highlighting
|
||||||
|
|
||||||
|
4. **Practical Tools Integration**:
|
||||||
|
|
||||||
|
- 🔍 Global Search Functionality
|
||||||
|
- 📝 Topic Management System
|
||||||
|
- 🔤 AI-powered Translation
|
||||||
|
- 🎯 Drag-and-drop Sorting
|
||||||
|
- 🔌 Mini Program Support
|
||||||
|
- ⚙️ MCP(Model Context Protocol) Server
|
||||||
|
|
||||||
|
5. **Enhanced User Experience**:
|
||||||
|
|
||||||
|
- 🖥️ Cross-platform Support for Windows, Mac, and Linux
|
||||||
|
- 📦 Ready to Use, No Environment Setup Required
|
||||||
|
- 🎨 Light/Dark Themes and Transparent Window
|
||||||
|
- 📝 Complete Markdown Rendering
|
||||||
|
- 🤲 Easy Content Sharing
|
||||||
|
|
||||||
|
# 📝 TODO
|
||||||
|
|
||||||
|
- [x] Quick popup (read clipboard, quick question, explain, translate, summarize)
|
||||||
|
- [x] Comparison of multi-model answers
|
||||||
|
- [x] Support login using SSO provided by service providers
|
||||||
|
- [x] All models support networking
|
||||||
|
- [x] Launch of the first official version
|
||||||
|
- [x] Bug fixes and improvements (In progress...)
|
||||||
|
- [ ] Plugin functionality (JavaScript)
|
||||||
|
- [ ] Browser extension (highlight text to translate, summarize, add to knowledge base)
|
||||||
|
- [ ] iOS & Android client
|
||||||
|
- [ ] AI notes
|
||||||
|
- [ ] Voice input and output (AI call)
|
||||||
|
- [ ] Data backup supports custom backup content
|
||||||
|
|
||||||
# 🖥️ Develop
|
# 🖥️ Develop
|
||||||
|
|
||||||
## IDE Setup
|
Refer to the [development documentation](docs/dev.md)
|
||||||
|
|
||||||
[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)
|
# 🤝 Contributing
|
||||||
|
|
||||||
## Project Setup
|
We welcome contributions to Cherry Studio! Here are some ways you can contribute:
|
||||||
|
|
||||||
### Install
|
1. **Contribute Code**: Develop new features or optimize existing code.
|
||||||
|
2. **Fix Bugs**: Submit fixes for any bugs you find.
|
||||||
|
3. **Maintain Issues**: Help manage GitHub issues.
|
||||||
|
4. **Product Design**: Participate in design discussions.
|
||||||
|
5. **Write Documentation**: Improve user manuals and guides.
|
||||||
|
6. **Community Engagement**: Join discussions and help users.
|
||||||
|
7. **Promote Usage**: Spread the word about Cherry Studio.
|
||||||
|
|
||||||
```bash
|
## Getting Started
|
||||||
$ yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
1. **Fork the Repository**: Fork and clone it to your local machine.
|
||||||
|
2. **Create a Branch**: For your changes.
|
||||||
|
3. **Submit Changes**: Commit and push your changes.
|
||||||
|
4. **Open a Pull Request**: Describe your changes and reasons.
|
||||||
|
|
||||||
```bash
|
For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIBUTING.md).
|
||||||
$ yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
Thank you for your support and contributions!
|
||||||
|
|
||||||
```bash
|
## Related Projects
|
||||||
# For windows
|
|
||||||
$ yarn build:win
|
|
||||||
|
|
||||||
# For macOS
|
- [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution.
|
||||||
$ yarn build:mac
|
|
||||||
|
|
||||||
# For Linux
|
- [ublacklist](https://github.com/iorate/ublacklist):Blocks specific sites from appearing in Google search results
|
||||||
$ yarn build:linux
|
|
||||||
```
|
|
||||||
|
|
||||||
# ⭐️ Star History
|
|
||||||
|
|
||||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
|
||||||
|
|
||||||
# 🚀 Contributors
|
# 🚀 Contributors
|
||||||
|
|
||||||
<a href="https://github.com/kangfenmao/cherry-studio/graphs/contributors">
|
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
||||||
</a>
|
</a>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
# Community
|
# 🌐 Community
|
||||||
|
|
||||||
[Telegram](https://t.me/CherryStudioAI)
|
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
|
||||||
|
|
||||||
# Sponsor
|
# 📣 Product Hunt
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
# ☕ Sponsor
|
||||||
|
|
||||||
[Buy Me a Coffee](docs/sponsor.md)
|
[Buy Me a Coffee](docs/sponsor.md)
|
||||||
|
|
||||||
# 📃 License
|
# 📃 License
|
||||||
|
|
||||||
[LICENSE](./LICENSE)
|
[LICENSE](./LICENSE)
|
||||||
|
|
||||||
|
# ⭐️ Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
<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)
|
|
||||||
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
;Inspired by:
|
||||||
|
; https://gist.github.com/bogdibota/062919938e1ed388b3db5ea31f52955c
|
||||||
|
; https://stackoverflow.com/questions/34177547/detect-if-visual-c-redistributable-for-visual-studio-2013-is-installed
|
||||||
|
; https://stackoverflow.com/a/54391388
|
||||||
|
; https://github.com/GitCommons/cpp-redist-nsis/blob/main/installer.nsh
|
||||||
|
|
||||||
|
;Find latests downloads here:
|
||||||
|
; https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
|
||||||
|
|
||||||
|
!include LogicLib.nsh
|
||||||
|
|
||||||
|
; https://github.com/electron-userland/electron-builder/issues/1122
|
||||||
|
!ifndef BUILD_UNINSTALLER
|
||||||
|
Function checkVCRedist
|
||||||
|
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
|
||||||
|
FunctionEnd
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro customInit
|
||||||
|
Push $0
|
||||||
|
Call checkVCRedist
|
||||||
|
${If} $0 != "1"
|
||||||
|
MessageBox MB_YESNO "\
|
||||||
|
NOTE: ${PRODUCT_NAME} requires $\r$\n\
|
||||||
|
'Microsoft Visual C++ Redistributable'$\r$\n\
|
||||||
|
to function properly.$\r$\n$\r$\n\
|
||||||
|
Download and install now?" /SD IDYES IDYES InstallVCRedist IDNO DontInstall
|
||||||
|
InstallVCRedist:
|
||||||
|
inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable..." "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe"
|
||||||
|
ExecWait "$TEMP\vc_redist.x64.exe /install /norestart"
|
||||||
|
;IfErrors InstallError ContinueInstall ; vc_redist exit code is unreliable :(
|
||||||
|
Call checkVCRedist
|
||||||
|
${If} $0 == "1"
|
||||||
|
Goto ContinueInstall
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
;InstallError:
|
||||||
|
MessageBox MB_ICONSTOP "\
|
||||||
|
There was an unexpected error installing$\r$\n\
|
||||||
|
Microsoft Visual C++ Redistributable.$\r$\n\
|
||||||
|
The installation of ${PRODUCT_NAME} cannot continue."
|
||||||
|
DontInstall:
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
ContinueInstall:
|
||||||
|
Pop $0
|
||||||
|
!macroend
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,6 +1,8 @@
|
|||||||
# provider: generic
|
# provider: generic
|
||||||
# url: http://127.0.0.1:8080
|
# url: http://127.0.0.1:8080
|
||||||
# updaterCacheDirName: cherry-studio-updater
|
# updaterCacheDirName: cherry-studio-updater
|
||||||
provider: github
|
# provider: github
|
||||||
repo: cherry-studio
|
# repo: cherry-studio
|
||||||
owner: kangfenmao
|
# owner: kangfenmao
|
||||||
|
provider: generic
|
||||||
|
url: https://cherrystudio.ocool.online
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<h1 align="center">
|
||||||
|
<a href="https://github.com/CherryHQ/cherry-studio/releases">
|
||||||
|
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 <br>
|
||||||
|
</p>
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
</div>
|
||||||
|
# 🍒 Cherry Studio
|
||||||
|
|
||||||
|
Cherry Studio は、複数の LLM プロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linux で利用可能です。
|
||||||
|
|
||||||
|
👏 [Telegram](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
|
||||||
|
|
||||||
|
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
|
||||||
|
|
||||||
|
# 🌠 スクリーンショット
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
# 🌟 主な機能
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **多様な LLM サービス対応**:
|
||||||
|
|
||||||
|
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
|
||||||
|
- 🔗 AI Web サービス統合:Claude、Peplexity、Poe など
|
||||||
|
- 💻 Ollama、LM Studio によるローカルモデル実行対応
|
||||||
|
|
||||||
|
2. **AI アシスタントと対話**:
|
||||||
|
|
||||||
|
- 📚 300+ の事前設定済み AI アシスタント
|
||||||
|
- 🤖 カスタム AI アシスタントの作成
|
||||||
|
- 💬 複数モデルでの同時対話機能
|
||||||
|
|
||||||
|
3. **文書とデータ処理**:
|
||||||
|
|
||||||
|
- 📄 テキスト、画像、Office、PDF など多様な形式対応
|
||||||
|
- ☁️ WebDAV によるファイル管理とバックアップ
|
||||||
|
- 📊 Mermaid による図表作成
|
||||||
|
- 💻 コードハイライト機能
|
||||||
|
|
||||||
|
4. **実用的なツール統合**:
|
||||||
|
|
||||||
|
- 🔍 グローバル検索機能
|
||||||
|
- 📝 トピック管理システム
|
||||||
|
- 🔤 AI による翻訳機能
|
||||||
|
- 🎯 ドラッグ&ドロップによる整理
|
||||||
|
- 🔌 ミニプログラム対応
|
||||||
|
- ⚙️ MCP(モデルコンテキストプロトコル) サービス
|
||||||
|
|
||||||
|
5. **優れたユーザー体験**:
|
||||||
|
|
||||||
|
- 🖥️ Windows、Mac、Linux のクロスプラットフォーム対応
|
||||||
|
- 📦 環境構築不要ですぐに使用可能
|
||||||
|
- 🎨 ライト/ダークテーマと透明ウィンドウ対応
|
||||||
|
- 📝 完全な Markdown レンダリング
|
||||||
|
- 🤲 簡単な共有機能
|
||||||
|
|
||||||
|
# 📝 TODO
|
||||||
|
|
||||||
|
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
|
||||||
|
- [x] 複数モデルの回答の比較
|
||||||
|
- [x] サービスプロバイダーが提供する SSO を使用したログインをサポート
|
||||||
|
- [x] すべてのモデルがネットワークをサポート
|
||||||
|
- [x] 最初の公式バージョンのリリース
|
||||||
|
- [ ] 錯誤修復と改善 (開発中...)
|
||||||
|
- [ ] プラグイン機能(JavaScript)
|
||||||
|
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
|
||||||
|
- [ ] iOS & Android クライアント
|
||||||
|
- [ ] AIノート
|
||||||
|
- [ ] 音声入出力(AI コール)
|
||||||
|
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
|
||||||
|
|
||||||
|
# 🖥️ 開発
|
||||||
|
|
||||||
|
参考[開発ドキュメント](dev.md)
|
||||||
|
|
||||||
|
# 🤝 貢献
|
||||||
|
|
||||||
|
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
|
||||||
|
|
||||||
|
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します。
|
||||||
|
2. **バグの修正**:見つけたバグを修正します。
|
||||||
|
3. **問題の管理**:GitHub の問題を管理するのを手伝います。
|
||||||
|
4. **製品デザイン**:デザインの議論に参加します。
|
||||||
|
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します。
|
||||||
|
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します。
|
||||||
|
7. **使用の促進**:Cherry Studio を広めます。
|
||||||
|
|
||||||
|
## 始め方
|
||||||
|
|
||||||
|
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします。
|
||||||
|
2. **ブランチを作成**:変更のためのブランチを作成します。
|
||||||
|
3. **変更を提出**:変更をコミットしてプッシュします。
|
||||||
|
4. **プルリクエストを開く**:変更内容と理由を説明します。
|
||||||
|
|
||||||
|
詳細なガイドラインについては、[貢献ガイド](../CONTRIBUTING.md)をご覧ください。
|
||||||
|
|
||||||
|
ご支援と貢献に感謝します!
|
||||||
|
|
||||||
|
## 関連頁版
|
||||||
|
|
||||||
|
- [one-api](https://github.com/songquanpeng/one-api):LLM API の管理・配信システム。OpenAI、Azure、Anthropic などの主要モデルに対応し、統一 API インターフェースを提供。API キー管理と再配布に利用可能。
|
||||||
|
|
||||||
|
# 🚀 コントリビューター
|
||||||
|
|
||||||
|
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
# コミュニティ
|
||||||
|
|
||||||
|
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
|
||||||
|
|
||||||
|
# 📣 プロダクトハント
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
# スポンサー
|
||||||
|
|
||||||
|
[Buy Me a Coffee](sponsor.md)
|
||||||
|
|
||||||
|
# 📃 ライセンス
|
||||||
|
|
||||||
|
[LICENSE](../LICENSE)
|
||||||
|
|
||||||
|
# ⭐️ スター履歴
|
||||||
|
|
||||||
|
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<h1 align="center">
|
||||||
|
<a href="https://github.com/CherryHQ/cherry-studio/releases">
|
||||||
|
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br></p>
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
</div>
|
||||||
|
# 🍒 Cherry Studio
|
||||||
|
|
||||||
|
Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。
|
||||||
|
|
||||||
|
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
|
||||||
|
|
||||||
|
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
|
||||||
|
|
||||||
|
# 🌠 界面
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
# 🌟 主要特性
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **多样化 LLM 服务支持**:
|
||||||
|
|
||||||
|
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||||
|
- 🔗 集成流行 AI Web 服务:Claude、Peplexity、Poe、腾讯元宝、知乎直答等
|
||||||
|
- 💻 支持 Ollama、LM Studio 本地模型部署
|
||||||
|
|
||||||
|
2. **智能助手与对话**:
|
||||||
|
|
||||||
|
- 📚 内置 300+ 预配置 AI 助手
|
||||||
|
- 🤖 支持自定义创建专属助手
|
||||||
|
- 💬 多模型同时对话,获得多样化观点
|
||||||
|
|
||||||
|
3. **文档与数据处理**:
|
||||||
|
|
||||||
|
- 📄 支持文本、图片、Office、PDF 等多种格式
|
||||||
|
- ☁️ WebDAV 文件管理与数据备份
|
||||||
|
- 📊 Mermaid 图表可视化
|
||||||
|
- 💻 代码高亮显示
|
||||||
|
|
||||||
|
4. **实用工具集成**:
|
||||||
|
|
||||||
|
- 🔍 全局搜索功能
|
||||||
|
- 📝 话题管理系统
|
||||||
|
- 🔤 AI 驱动的翻译功能
|
||||||
|
- 🎯 拖拽排序
|
||||||
|
- 🔌 小程序支持
|
||||||
|
- ⚙️ MCP(模型上下文协议) 服务
|
||||||
|
|
||||||
|
5. **优质使用体验**:
|
||||||
|
|
||||||
|
- 🖥️ Windows、Mac、Linux 跨平台支持
|
||||||
|
- 📦 开箱即用,无需配置环境
|
||||||
|
- 🎨 支持明暗主题与透明窗口
|
||||||
|
- 📝 完整的 Markdown 渲染
|
||||||
|
- 🤲 便捷的内容分享功能
|
||||||
|
|
||||||
|
# 📝 待辦事項
|
||||||
|
|
||||||
|
- [x] 快捷弹窗(读取剪贴板、快速提问、解释、翻译、总结)
|
||||||
|
- [x] 多模型回答对比
|
||||||
|
- [x] 支持使用服务供应商提供的 SSO 进行登入
|
||||||
|
- [x] 全部模型支持连网(开发中...)
|
||||||
|
- [x] 推出第一个正式版
|
||||||
|
- [x] 错误修复和改进(开发中...)
|
||||||
|
- [ ] 插件功能(JavaScript)
|
||||||
|
- [ ] 浏览器插件(划词翻译、总结、新增至知识库)
|
||||||
|
- [ ] iOS & Android 客户端
|
||||||
|
- [ ] AI 笔记
|
||||||
|
- [ ] 语音输入输出(AI 通话)
|
||||||
|
- [ ] 数据备份支持自定义备份内容
|
||||||
|
|
||||||
|
# 🖥️ 开发
|
||||||
|
|
||||||
|
参考[开发文档](dev.md)
|
||||||
|
|
||||||
|
# 🤝 贡献
|
||||||
|
|
||||||
|
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
|
||||||
|
|
||||||
|
1. **贡献代码**:开发新功能或优化现有代码。
|
||||||
|
2. **修复错误**:提交您发现的错误修复。
|
||||||
|
3. **维护问题**:帮助管理 GitHub 问题。
|
||||||
|
4. **产品设计**:参与设计讨论。
|
||||||
|
5. **撰写文档**:改进用户手册和指南。
|
||||||
|
6. **社区参与**:加入讨论并帮助用户。
|
||||||
|
7. **推广使用**:宣传 Cherry Studio。
|
||||||
|
|
||||||
|
## 入门
|
||||||
|
|
||||||
|
1. **Fork 仓库**:Fork 并克隆到您的本地机器。
|
||||||
|
2. **创建分支**:为您的更改创建分支。
|
||||||
|
3. **提交更改**:提交并推送您的更改。
|
||||||
|
4. **打开 Pull Request**:描述您的更改和原因。
|
||||||
|
|
||||||
|
有关更详细的指南,请参阅我们的 [贡献指南](../CONTRIBUTING.md)。
|
||||||
|
|
||||||
|
感谢您的支持和贡献!
|
||||||
|
|
||||||
|
## 相关项目
|
||||||
|
|
||||||
|
- [one-api](https://github.com/songquanpeng/one-api):LLM API 管理及分发系统,支持 OpenAI、Azure、Anthropic 等主流模型,统一 API 接口,可用于密钥管理与二次分发。
|
||||||
|
|
||||||
|
# 🚀 贡献者
|
||||||
|
|
||||||
|
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
||||||
|
</a>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
# 🌐 社区
|
||||||
|
|
||||||
|
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
|
||||||
|
|
||||||
|
# 📣 产品猎人
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
# ☕ 赞助
|
||||||
|
|
||||||
|
[微信赞赏码](sponsor.md)
|
||||||
|
|
||||||
|
# 📃 许可证
|
||||||
|
|
||||||
|
[LICENSE](../LICENSE)
|
||||||
|
|
||||||
|
# ⭐️ Star 记录
|
||||||
|
|
||||||
|
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 🖥️ Develop
|
||||||
|
|
||||||
|
## IDE Setup
|
||||||
|
|
||||||
|
[Cursor](https://www.cursor.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
### Setup Node.js
|
||||||
|
|
||||||
|
Download and install [Node.js v20.x.x](https://nodejs.org/en/download)
|
||||||
|
|
||||||
|
### Setup Yarn
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack enable
|
||||||
|
corepack prepare yarn@4.6.0 --activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For windows
|
||||||
|
$ yarn build:win
|
||||||
|
|
||||||
|
# For macOS
|
||||||
|
$ yarn build:mac
|
||||||
|
|
||||||
|
# For Linux
|
||||||
|
$ yarn build:linux
|
||||||
|
```
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# 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. 确保测试覆盖主要功能和组件
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
## 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`中管理数据库结构变更
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
我来详细介绍 KnowledgeService 中控制并发处理的项目数量和工作负载的部分。
|
||||||
|
|
||||||
|
## 并发控制与工作负载管理机制
|
||||||
|
|
||||||
|
KnowledgeService 实现了一个精细的任务队列系统,用于控制并发处理的项目数量和工作负载。这个系统主要通过以下几个关键部分实现:
|
||||||
|
|
||||||
|
### 1. 关键变量和限制
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private workload = 0
|
||||||
|
private processingItemCount = 0
|
||||||
|
private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map()
|
||||||
|
private static MAXIMUM_WORKLOAD = 1024 * 1024 * 80 // 约80MB
|
||||||
|
private static MAXIMUM_PROCESSING_ITEM_COUNT = 30
|
||||||
|
```
|
||||||
|
|
||||||
|
- `workload`: 跟踪当前正在处理的总工作量(以字节为单位)
|
||||||
|
- `processingItemCount`: 跟踪当前正在处理的项目数量
|
||||||
|
- `MAXIMUM_WORKLOAD`: 设置最大工作负载为80MB
|
||||||
|
- `MAXIMUM_PROCESSING_ITEM_COUNT`: 设置最大并发处理项目数为30个
|
||||||
|
|
||||||
|
### 2. 工作负载评估
|
||||||
|
|
||||||
|
每个任务都有一个评估工作负载的机制,通过 `evaluateTaskWorkload` 属性来表示:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface EvaluateTaskWorkload {
|
||||||
|
workload: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
不同类型的任务有不同的工作负载评估方式:
|
||||||
|
|
||||||
|
- 文件任务:使用文件大小作为工作负载 `{ workload: file.size }`
|
||||||
|
- URL任务:使用固定值 `{ workload: 1024 * 1024 * 2 }` (约2MB)
|
||||||
|
- 网站地图任务:使用固定值 `{ workload: 1024 * 1024 * 20 }` (约20MB)
|
||||||
|
- 笔记任务:使用文本内容的字节长度 `{ workload: contentBytes.length }`
|
||||||
|
|
||||||
|
### 3. 任务状态管理
|
||||||
|
|
||||||
|
任务通过状态枚举来跟踪其生命周期:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum LoaderTaskItemState {
|
||||||
|
PENDING, // 等待处理
|
||||||
|
PROCESSING, // 正在处理
|
||||||
|
DONE // 已完成
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 任务队列处理核心逻辑
|
||||||
|
|
||||||
|
核心的队列处理逻辑在 `processingQueueHandle` 方法中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private processingQueueHandle() {
|
||||||
|
const getSubtasksUntilMaximumLoad = (): QueueTaskItem[] => {
|
||||||
|
const queueTaskList: QueueTaskItem[] = []
|
||||||
|
that: for (const [task, resolve] of this.knowledgeItemProcessingQueueMappingPromise) {
|
||||||
|
for (const item of task.loaderTasks) {
|
||||||
|
if (this.maximumLoad()) {
|
||||||
|
break that
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state, task: taskPromise, evaluateTaskWorkload } = item
|
||||||
|
|
||||||
|
if (state !== LoaderTaskItemState.PENDING) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workload } = evaluateTaskWorkload
|
||||||
|
this.workload += workload
|
||||||
|
this.processingItemCount += 1
|
||||||
|
item.state = LoaderTaskItemState.PROCESSING
|
||||||
|
queueTaskList.push({
|
||||||
|
taskPromise: () =>
|
||||||
|
taskPromise().then(() => {
|
||||||
|
this.workload -= workload
|
||||||
|
this.processingItemCount -= 1
|
||||||
|
task.loaderTasks.delete(item)
|
||||||
|
if (task.loaderTasks.size === 0) {
|
||||||
|
this.knowledgeItemProcessingQueueMappingPromise.delete(task)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
this.processingQueueHandle()
|
||||||
|
}),
|
||||||
|
resolve: () => {},
|
||||||
|
evaluateTaskWorkload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queueTaskList
|
||||||
|
}
|
||||||
|
|
||||||
|
const subTasks = getSubtasksUntilMaximumLoad()
|
||||||
|
if (subTasks.length > 0) {
|
||||||
|
const subTaskPromises = subTasks.map(({ taskPromise }) => taskPromise())
|
||||||
|
Promise.all(subTaskPromises).then(() => {
|
||||||
|
subTasks.forEach(({ resolve }) => resolve())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个方法的工作流程是:
|
||||||
|
|
||||||
|
1. 遍历所有待处理的任务集合
|
||||||
|
2. 对于每个任务集合中的每个子任务:
|
||||||
|
- 检查是否已达到最大负载(通过 `maximumLoad()` 方法)
|
||||||
|
- 如果任务状态为 PENDING,则:
|
||||||
|
- 增加当前工作负载和处理项目计数
|
||||||
|
- 将任务状态更新为 PROCESSING
|
||||||
|
- 将任务添加到待执行队列
|
||||||
|
3. 执行所有收集到的子任务
|
||||||
|
4. 当子任务完成时:
|
||||||
|
- 减少工作负载和处理项目计数
|
||||||
|
- 从任务集合中移除已完成的任务
|
||||||
|
- 如果任务集合为空,则解析相应的 Promise
|
||||||
|
- 递归调用 `processingQueueHandle()` 以处理更多任务
|
||||||
|
|
||||||
|
### 5. 负载检查
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private maximumLoad() {
|
||||||
|
return (
|
||||||
|
this.processingItemCount >= KnowledgeService.MAXIMUM_PROCESSING_ITEM_COUNT ||
|
||||||
|
this.workload >= KnowledgeService.MAXIMUM_WORKLOAD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个方法检查当前是否已达到最大负载,通过两个条件:
|
||||||
|
|
||||||
|
- 处理项目数量达到上限(30个)
|
||||||
|
- 总工作负载达到上限(80MB)
|
||||||
|
|
||||||
|
### 6. 任务添加与执行流程
|
||||||
|
|
||||||
|
当添加新任务时,流程如下:
|
||||||
|
|
||||||
|
1. 创建任务(根据类型不同创建不同的任务)
|
||||||
|
2. 通过 `appendProcessingQueue` 将任务添加到队列
|
||||||
|
3. 调用 `processingQueueHandle` 开始处理队列中的任务
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private appendProcessingQueue(task: LoaderTask): Promise<LoaderReturn> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.knowledgeItemProcessingQueueMappingPromise.set(loaderTaskIntoOfSet(task), () => {
|
||||||
|
resolve(task.loaderDoneReturn!)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 并发控制的优势
|
||||||
|
|
||||||
|
这种并发控制机制有几个重要优势:
|
||||||
|
|
||||||
|
1. **资源使用优化**:通过限制同时处理的项目数量和总工作负载,避免系统资源过度使用
|
||||||
|
2. **自动调节**:当任务完成时,会自动从队列中获取新任务,保持资源的高效利用
|
||||||
|
3. **灵活性**:不同类型的任务有不同的工作负载评估,更准确地反映实际资源需求
|
||||||
|
4. **可靠性**:通过状态管理和Promise解析机制,确保任务正确完成并通知调用者
|
||||||
|
|
||||||
|
## 实际应用场景
|
||||||
|
|
||||||
|
这种并发控制在处理大量数据时特别有用,例如:
|
||||||
|
|
||||||
|
- 导入大型目录时,可能包含数百个文件
|
||||||
|
- 处理大型网站地图,可能包含大量URL
|
||||||
|
- 处理多个用户同时添加知识库项目的请求
|
||||||
|
|
||||||
|
通过这种机制,系统可以平滑地处理大量请求,避免资源耗尽,同时保持良好的响应性。
|
||||||
|
|
||||||
|
总结来说,KnowledgeService 实现了一个复杂而高效的任务队列系统,通过精确控制并发处理的项目数量和工作负载,确保系统在处理大量数据时保持稳定和高效。
|
||||||
@@ -11,10 +11,31 @@ files:
|
|||||||
- '!src'
|
- '!src'
|
||||||
- '!scripts'
|
- '!scripts'
|
||||||
- '!local'
|
- '!local'
|
||||||
|
- '!docs'
|
||||||
|
- '!packages'
|
||||||
|
- '!stats.html'
|
||||||
|
- '!*.md'
|
||||||
|
- '!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}'
|
||||||
|
- '!**/{test,tests,__tests__,coverage}/**'
|
||||||
|
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
|
||||||
|
- '!**/*.min.*.map'
|
||||||
|
- '!**/*.d.ts'
|
||||||
|
- '!**/{.DS_Store,Thumbs.db}'
|
||||||
|
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
|
||||||
|
- '!node_modules/rollup-plugin-visualizer'
|
||||||
|
- '!node_modules/js-tiktoken'
|
||||||
|
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
|
||||||
|
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
|
||||||
|
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
|
- '**/*.{node,dll,metal,exp,lib}'
|
||||||
win:
|
win:
|
||||||
executableName: Cherry Studio
|
executableName: Cherry Studio
|
||||||
|
artifactName: ${productName}-${version}-portable.${ext}
|
||||||
|
target:
|
||||||
|
- target: nsis
|
||||||
|
- target: portable
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${productName}-${version}-setup.${ext}
|
artifactName: ${productName}-${version}-setup.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
@@ -22,14 +43,16 @@ nsis:
|
|||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
oneClick: false
|
oneClick: false
|
||||||
|
include: build/nsis-installer.nsh
|
||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
notarize: false
|
||||||
|
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||||
extendInfo:
|
extendInfo:
|
||||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
notarize: false
|
|
||||||
target:
|
target:
|
||||||
- target: dmg
|
- target: dmg
|
||||||
arch:
|
arch:
|
||||||
@@ -39,30 +62,29 @@ mac:
|
|||||||
arch:
|
arch:
|
||||||
- arm64
|
- arm64
|
||||||
- x64
|
- x64
|
||||||
dmg:
|
|
||||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
|
||||||
linux:
|
linux:
|
||||||
|
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||||
target:
|
target:
|
||||||
- target: AppImage
|
- target: AppImage
|
||||||
arch:
|
arch:
|
||||||
- arm64
|
- arm64
|
||||||
- x64
|
- x64
|
||||||
# - snap
|
|
||||||
# - deb
|
|
||||||
maintainer: electronjs.org
|
maintainer: electronjs.org
|
||||||
category: Utility
|
category: Utility
|
||||||
appImage:
|
|
||||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
|
||||||
npmRebuild: false
|
|
||||||
publish:
|
publish:
|
||||||
|
# provider: generic
|
||||||
|
# url: https://cherrystudio.ocool.online
|
||||||
provider: github
|
provider: github
|
||||||
repo: cherry-studio
|
repo: cherry-studio
|
||||||
owner: kangfenmao
|
owner: CherryHQ
|
||||||
electronDownload:
|
electronDownload:
|
||||||
mirror: https://npmmirror.com/mirrors/electron/
|
mirror: https://npmmirror.com/mirrors/electron/
|
||||||
|
afterPack: scripts/after-pack.js
|
||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
增加数学公式渲染引擎切换
|
知识库设置增加重排模型,提升知识库的准确性
|
||||||
多个系统滚动条样式统一
|
自定义服务商增加兼容模式
|
||||||
修复添加默认助手会添加两个
|
增加 Github Copilot 服务商
|
||||||
|
PlantUML 预览支持放大和缩小
|
||||||
|
联网模式支持增强模式
|
||||||
|
|||||||
@@ -1,14 +1,42 @@
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
|
||||||
|
const visualizerPlugin = (type: 'renderer' | 'main') => {
|
||||||
|
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [
|
||||||
|
externalizeDepsPlugin({
|
||||||
|
exclude: [
|
||||||
|
'@llm-tools/embedjs',
|
||||||
|
'@llm-tools/embedjs-openai',
|
||||||
|
'@llm-tools/embedjs-loader-web',
|
||||||
|
'@llm-tools/embedjs-loader-markdown',
|
||||||
|
'@llm-tools/embedjs-loader-msoffice',
|
||||||
|
'@llm-tools/embedjs-loader-xml',
|
||||||
|
'@llm-tools/embedjs-loader-pdf',
|
||||||
|
'@llm-tools/embedjs-loader-sitemap',
|
||||||
|
'@llm-tools/embedjs-libsql',
|
||||||
|
'@llm-tools/embedjs-loader-image',
|
||||||
|
'p-queue'
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
...visualizerPlugin('main')
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
'@main': resolve('src/main'),
|
||||||
'@types': resolve('src/renderer/src/types'),
|
'@types': resolve('src/renderer/src/types'),
|
||||||
'@main': resolve('src/main')
|
'@shared': resolve('packages/shared')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['@libsql/client']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -16,11 +44,32 @@ export default defineConfig({
|
|||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
|
plugins: [
|
||||||
|
react({
|
||||||
|
babel: {
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
'styled-components',
|
||||||
|
{
|
||||||
|
displayName: true, // 开发环境下启用组件名称
|
||||||
|
fileName: false, // 不在类名中包含文件名
|
||||||
|
pure: true, // 优化性能
|
||||||
|
ssr: false // 不需要服务端渲染
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...visualizerPlugin('renderer')
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@renderer': resolve('src/renderer/src')
|
'@renderer': resolve('src/renderer/src'),
|
||||||
|
'@shared': resolve('packages/shared')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [react()]
|
optimizeDeps: {
|
||||||
|
exclude: ['chunk-PZ64DZKH.js', 'chunk-JMKENWIY.js', 'chunk-UXYB6GHG.js', 'chunk-ALDIEZMG.js', 'chunk-4X6ZJEXY.js']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import electronConfigPrettier from '@electron-toolkit/eslint-config-prettier'
|
||||||
|
import tseslint from '@electron-toolkit/eslint-config-ts'
|
||||||
|
import eslint from '@eslint/js'
|
||||||
|
import eslintReact from '@eslint-react/eslint-plugin'
|
||||||
|
import { defineConfig } from 'eslint/config'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
||||||
|
import unusedImports from 'eslint-plugin-unused-imports'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
eslint.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
electronConfigPrettier,
|
||||||
|
eslintReact.configs['recommended-typescript'],
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'simple-import-sort': simpleImportSort,
|
||||||
|
'unused-imports': unusedImports
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||||
|
'simple-import-sort/imports': 'error',
|
||||||
|
'simple-import-sort/exports': 'error',
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
|
'@eslint-react/no-prop-types': 'error',
|
||||||
|
'prettier/prettier': ['error', { endOfLine: 'auto' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { caughtErrors: 'none' }],
|
||||||
|
'@typescript-eslint/no-unused-expressions': 'off',
|
||||||
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
|
'@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 'off',
|
||||||
|
'@eslint-react/web-api/no-leaked-event-listener': 'off',
|
||||||
|
'@eslint-react/web-api/no-leaked-timeout': 'off',
|
||||||
|
'@eslint-react/no-unknown-property': 'off',
|
||||||
|
'@eslint-react/no-nested-component-definitions': 'off',
|
||||||
|
'@eslint-react/dom/no-dangerously-set-innerhtml': 'off',
|
||||||
|
'@eslint-react/no-array-index-key': 'off',
|
||||||
|
'@eslint-react/no-unstable-default-props': 'off',
|
||||||
|
'@eslint-react/no-unstable-context-value': 'off',
|
||||||
|
'@eslint-react/hooks-extra/prefer-use-state-lazy-initialization': 'off',
|
||||||
|
'@eslint-react/hooks-extra/no-unnecessary-use-prefix': 'off',
|
||||||
|
'@eslint-react/no-children-to-array': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
ignores: ['node_modules/**', 'dist/**', 'out/**', 'local/**', '.gitignore', 'scripts/cloudflare-worker.js']
|
||||||
|
}
|
||||||
|
])
|
||||||
@@ -1,115 +1,171 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "0.8.3",
|
"version": "1.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "kangfenmao@qq.com",
|
"author": "support@cherry-ai.com",
|
||||||
"homepage": "https://github.com/kangfenmao/cherry-studio",
|
"homepage": "https://github.com/CherryHQ/cherry-studio",
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"local",
|
"local",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"nohoist": [
|
"installConfig": {
|
||||||
"packages/database"
|
"hoistingLimits": [
|
||||||
]
|
"packages/database"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
|
||||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
|
||||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
|
||||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
|
||||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"build": "npm run typecheck && electron-vite build",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"build:check": "yarn test && yarn typecheck && yarn check:i18n",
|
||||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||||
"build:win": "dotenv npm run build && electron-builder --win --publish never",
|
"build:win": "dotenv npm run build && electron-builder --win",
|
||||||
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
|
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
||||||
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
|
"build:mac": "dotenv electron-vite build && electron-builder --mac",
|
||||||
|
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
|
||||||
|
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
|
||||||
|
"build:linux": "dotenv electron-vite build && electron-builder --linux",
|
||||||
|
"build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64",
|
||||||
|
"build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64",
|
||||||
|
"build:npm": "node scripts/build-npm.js",
|
||||||
"release": "node scripts/version.js",
|
"release": "node scripts/version.js",
|
||||||
"publish": "yarn release patch push",
|
"publish": "yarn build:check && yarn release patch push",
|
||||||
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
||||||
"generate:agents": "yarn workspace @cherry-studio/database agents",
|
"generate:agents": "yarn workspace @cherry-studio/database agents",
|
||||||
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build"
|
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
||||||
|
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
||||||
|
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
||||||
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
|
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
|
"check:i18n": "node scripts/check-i18n.js",
|
||||||
|
"test": "npx -y tsx --test src/**/*.test.ts",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||||
|
"postinstall": "electron-builder install-app-deps",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@agentic/exa": "^7.3.3",
|
||||||
|
"@agentic/searxng": "^7.3.3",
|
||||||
|
"@agentic/tavily": "^7.3.3",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"archiver": "^7.0.1",
|
"@electron/notarize": "^2.5.0",
|
||||||
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
|
"@google/generative-ai": "^0.21.0",
|
||||||
|
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.28-8e4393fa2d.patch",
|
||||||
|
"@llm-tools/embedjs-libsql": "^0.1.28",
|
||||||
|
"@llm-tools/embedjs-loader-csv": "^0.1.28",
|
||||||
|
"@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.28-81647ffac6.patch",
|
||||||
|
"@llm-tools/embedjs-loader-msoffice": "^0.1.28",
|
||||||
|
"@llm-tools/embedjs-loader-pdf": "^0.1.28",
|
||||||
|
"@llm-tools/embedjs-loader-sitemap": "^0.1.28",
|
||||||
|
"@llm-tools/embedjs-loader-web": "^0.1.28",
|
||||||
|
"@llm-tools/embedjs-loader-xml": "^0.1.28",
|
||||||
|
"@llm-tools/embedjs-openai": "^0.1.28",
|
||||||
|
"@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch",
|
||||||
|
"@notionhq/client": "^2.2.15",
|
||||||
|
"@tryfabric/martian": "^1.2.4",
|
||||||
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
|
"apache-arrow": "^18.1.0",
|
||||||
|
"docx": "^9.0.2",
|
||||||
"electron-log": "^5.1.5",
|
"electron-log": "^5.1.5",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
|
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||||
|
"fetch-socks": "^1.3.2",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"html2canvas": "^1.4.1",
|
"markdown-it": "^14.1.0",
|
||||||
|
"npx-scope-finder": "^1.2.0",
|
||||||
"officeparser": "^4.1.1",
|
"officeparser": "^4.1.1",
|
||||||
"unzipper": "^0.12.3",
|
"p-queue": "^8.1.0",
|
||||||
"webdav": "4.11.4"
|
"socks-proxy-agent": "^8.0.3",
|
||||||
|
"tar": "^7.4.3",
|
||||||
|
"tokenx": "^0.4.1",
|
||||||
|
"undici": "^7.4.0",
|
||||||
|
"webdav": "4.11.4",
|
||||||
|
"zipread": "^1.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.24.3",
|
"@anthropic-ai/sdk": "^0.38.0",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||||
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@google/generative-ai": "^0.16.0",
|
"@eslint-react/eslint-plugin": "^1.36.1",
|
||||||
|
"@eslint/js": "^9.22.0",
|
||||||
"@hello-pangea/dnd": "^16.6.0",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||||
|
"@llm-tools/embedjs-loader-image": "^0.1.28",
|
||||||
"@reduxjs/toolkit": "^2.2.5",
|
"@reduxjs/toolkit": "^2.2.5",
|
||||||
|
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||||
|
"@types/adm-zip": "^0",
|
||||||
"@types/fs-extra": "^11",
|
"@types/fs-extra": "^11",
|
||||||
"@types/lodash": "^4.17.5",
|
"@types/lodash": "^4.17.5",
|
||||||
|
"@types/markdown-it": "^14",
|
||||||
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^18.19.9",
|
"@types/node": "^18.19.9",
|
||||||
|
"@types/pako": "^1.0.2",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
"@types/tinycolor2": "^1",
|
"@types/tinycolor2": "^1",
|
||||||
"@types/unzipper": "^0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"antd": "^5.18.3",
|
"antd": "^5.22.5",
|
||||||
|
"applescript": "^1.0.0",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
|
"babel-plugin-styled-components": "^2.1.4",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"electron": "^28.3.3",
|
"electron": "31.7.6",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.3.0",
|
||||||
"emittery": "^1.0.3",
|
"emittery": "^1.0.3",
|
||||||
"emoji-picker-element": "^1.22.1",
|
"emoji-picker-element": "^1.22.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^9.22.0",
|
||||||
"eslint-plugin-react": "^7.34.3",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.0.0",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"gpt-tokens": "^1.3.10",
|
"html-to-image": "^1.11.13",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"localforage": "^1.10.0",
|
"lint-staged": "^15.5.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"openai": "^4.52.1",
|
"openai": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.5.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-custom-scrollbars-2": "^4.5.0",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"react-i18next": "^14.1.2",
|
"react-i18next": "^14.1.2",
|
||||||
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
"react-router-dom": "6",
|
"react-router-dom": "6",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-mathjax": "^6.0.0",
|
"rehype-mathjax": "^7.0.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"sass": "^1.77.2",
|
"sass": "^1.77.2",
|
||||||
|
"shiki": "^1.22.2",
|
||||||
|
"string-width": "^7.2.0",
|
||||||
"styled-components": "^6.1.11",
|
"styled-components": "^6.1.11",
|
||||||
"tinycolor2": "^1.6.0",
|
"tinycolor2": "^1.6.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
@@ -121,7 +177,19 @@
|
|||||||
"react-dom": "^17.0.0 || ^18.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
|
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
||||||
|
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||||
|
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||||
|
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.5.0"
|
"packageManager": "yarn@4.6.0",
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix"
|
||||||
|
],
|
||||||
|
"*.{json,md,yml,yaml,css,scss,html}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "@cherry-studio/artifacts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@cherry-studio/artifacts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cherry-studio/database",
|
"name": "@cherry-studio/database",
|
||||||
"packageManager": "yarn@4.3.1",
|
"packageManager": "yarn@4.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ db.all('SELECT * FROM agents', [], (err, rows) => {
|
|||||||
// 将 ID 类型转换为字符串
|
// 将 ID 类型转换为字符串
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
row.id = row.id.toString()
|
row.id = row.id.toString()
|
||||||
|
row.group = row.group.toString().split(',')
|
||||||
|
row.group = row.group.map((item) => item.trim().replace('\r\n', ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将查询结果转换为JSON字符串
|
// 将查询结果转换为JSON字符串
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||||
|
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||||
|
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||||
|
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||||
|
export const thirdPartyApplicationExts = ['.draftsExport']
|
||||||
|
export const bookExts = ['.epub']
|
||||||
|
export const textExts = [
|
||||||
|
'.txt', // 普通文本文件
|
||||||
|
'.md', // Markdown 文件
|
||||||
|
'.mdx', // Markdown 文件
|
||||||
|
'.html', // HTML 文件
|
||||||
|
'.htm', // HTML 文件的另一种扩展名
|
||||||
|
'.xml', // XML 文件
|
||||||
|
'.json', // JSON 文件
|
||||||
|
'.yaml', // YAML 文件
|
||||||
|
'.yml', // YAML 文件的另一种扩展名
|
||||||
|
'.csv', // 逗号分隔值文件
|
||||||
|
'.tsv', // 制表符分隔值文件
|
||||||
|
'.ini', // 配置文件
|
||||||
|
'.log', // 日志文件
|
||||||
|
'.rtf', // 富文本格式文件
|
||||||
|
'.org', // org-mode 文件
|
||||||
|
'.wiki', // VimWiki 文件
|
||||||
|
'.tex', // LaTeX 文件
|
||||||
|
'.bib', // BibTeX 文件
|
||||||
|
'.srt', // 字幕文件
|
||||||
|
'.xhtml', // XHTML 文件
|
||||||
|
'.nfo', // 信息文件(主要用于场景发布)
|
||||||
|
'.conf', // 配置文件
|
||||||
|
'.config', // 配置文件
|
||||||
|
'.env', // 环境变量文件
|
||||||
|
'.rst', // reStructuredText 文件
|
||||||
|
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||||
|
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||||
|
'.ts', // TypeScript 文件
|
||||||
|
'.jsp', // JavaServer Pages 文件
|
||||||
|
'.aspx', // ASP.NET 文件
|
||||||
|
'.bat', // Windows 批处理文件
|
||||||
|
'.sh', // Unix/Linux Shell 脚本文件
|
||||||
|
'.py', // Python 脚本文件
|
||||||
|
'.ipynb', // Jupyter 笔记本格式
|
||||||
|
'.rb', // Ruby 脚本文件
|
||||||
|
'.pl', // Perl 脚本文件
|
||||||
|
'.sql', // SQL 脚本文件
|
||||||
|
'.css', // Cascading Style Sheets 文件
|
||||||
|
'.less', // Less CSS 预处理器文件
|
||||||
|
'.scss', // Sass CSS 预处理器文件
|
||||||
|
'.sass', // Sass 文件
|
||||||
|
'.styl', // Stylus CSS 预处理器文件
|
||||||
|
'.coffee', // CoffeeScript 文件
|
||||||
|
'.ino', // Arduino 代码文件
|
||||||
|
'.asm', // Assembly 语言文件
|
||||||
|
'.go', // Go 语言文件
|
||||||
|
'.scala', // Scala 语言文件
|
||||||
|
'.swift', // Swift 语言文件
|
||||||
|
'.kt', // Kotlin 语言文件
|
||||||
|
'.rs', // Rust 语言文件
|
||||||
|
'.lua', // Lua 语言文件
|
||||||
|
'.groovy', // Groovy 语言文件
|
||||||
|
'.dart', // Dart 语言文件
|
||||||
|
'.hs', // Haskell 语言文件
|
||||||
|
'.clj', // Clojure 语言文件
|
||||||
|
'.cljs', // ClojureScript 语言文件
|
||||||
|
'.elm', // Elm 语言文件
|
||||||
|
'.erl', // Erlang 语言文件
|
||||||
|
'.ex', // Elixir 语言文件
|
||||||
|
'.exs', // Elixir 脚本文件
|
||||||
|
'.pug', // Pug (formerly Jade) 模板文件
|
||||||
|
'.haml', // Haml 模板文件
|
||||||
|
'.slim', // Slim 模板文件
|
||||||
|
'.tpl', // 模板文件(通用)
|
||||||
|
'.ejs', // Embedded JavaScript 模板文件
|
||||||
|
'.hbs', // Handlebars 模板文件
|
||||||
|
'.mustache', // Mustache 模板文件
|
||||||
|
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||||
|
'.twig', // Twig 模板文件
|
||||||
|
'.blade', // Blade 模板文件 (Laravel)
|
||||||
|
'.vue', // Vue.js 单文件组件
|
||||||
|
'.jsx', // React JSX 文件
|
||||||
|
'.tsx', // React TSX 文件
|
||||||
|
'.graphql', // GraphQL 查询语言文件
|
||||||
|
'.gql', // GraphQL 查询语言文件
|
||||||
|
'.proto', // Protocol Buffers 文件
|
||||||
|
'.thrift', // Thrift 文件
|
||||||
|
'.toml', // TOML 配置文件
|
||||||
|
'.edn', // Clojure 数据表示文件
|
||||||
|
'.cake', // CakePHP 配置文件
|
||||||
|
'.ctp', // CakePHP 视图文件
|
||||||
|
'.cfm', // ColdFusion 标记语言文件
|
||||||
|
'.cfc', // ColdFusion 组件文件
|
||||||
|
'.m', // Objective-C 源文件
|
||||||
|
'.mm', // Objective-C++ 源文件
|
||||||
|
'.gradle', // Gradle 构建文件
|
||||||
|
'.groovy', // Gradle 构建文件
|
||||||
|
'.kts', // Kotlin Script 文件
|
||||||
|
'.java', // Java 代码文件
|
||||||
|
'.cs', // C# 代码文件
|
||||||
|
'.cpp', // C++ 代码文件
|
||||||
|
'.c', // C++ 代码文件
|
||||||
|
'.h', // C++ 头文件
|
||||||
|
'.hpp', // C++ 头文件
|
||||||
|
'.cc', // C++ 源文件
|
||||||
|
'.cxx', // C++ 源文件
|
||||||
|
'.cppm', // C++20 模块接口文件
|
||||||
|
'.ipp', // 模板实现文件
|
||||||
|
'.ixx', // C++20 模块实现文件
|
||||||
|
'.f90', // Fortran 90 源文件
|
||||||
|
'.f', // Fortran 固定格式源代码文件
|
||||||
|
'.f03' // Fortran 2003+ 源代码文件
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ZOOM_SHORTCUTS = [
|
||||||
|
{
|
||||||
|
key: 'zoom_in',
|
||||||
|
shortcut: ['CommandOrControl', '='],
|
||||||
|
editable: false,
|
||||||
|
enabled: true,
|
||||||
|
system: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'zoom_out',
|
||||||
|
shortcut: ['CommandOrControl', '-'],
|
||||||
|
editable: false,
|
||||||
|
enabled: true,
|
||||||
|
system: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'zoom_reset',
|
||||||
|
shortcut: ['CommandOrControl', '0'],
|
||||||
|
editable: false,
|
||||||
|
enabled: true,
|
||||||
|
system: true
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export type LoaderReturn = {
|
||||||
|
entriesAdded: number
|
||||||
|
uniqueId: string
|
||||||
|
uniqueIds: string[]
|
||||||
|
loaderType: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Github Releases Timeline</title>
|
||||||
|
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.10/dist/typography.min.css"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="app">
|
||||||
|
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
|
||||||
|
<div class="max-w-3xl mx-auto py-12 px-4">
|
||||||
|
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
||||||
|
|
||||||
|
<!-- Loading状态 -->
|
||||||
|
<div v-if="loading" class="text-center py-8">
|
||||||
|
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
||||||
|
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error 状态 -->
|
||||||
|
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
|
||||||
|
|
||||||
|
<!-- Release 列表 -->
|
||||||
|
<div v-else class="space-y-8">
|
||||||
|
<div v-for="release in releases" :key="release.id" class="relative pl-8"
|
||||||
|
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
||||||
|
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
||||||
|
<div class="rounded-lg shadow-sm p-6 transition-shadow"
|
||||||
|
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
|
||||||
|
{{ release.name || release.tag_name }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
||||||
|
{{ formatDate(release.published_at) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||||
|
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
||||||
|
{{ release.tag_name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
|
||||||
|
v-html="renderMarkdown(release.body)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const md = window.markdownit({
|
||||||
|
breaks: true,
|
||||||
|
linkify: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const { createApp } = Vue
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
releases: [],
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
isDark: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchReleases() {
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch releases')
|
||||||
|
}
|
||||||
|
this.releases = await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
this.error = 'Error loading releases: ' + err.message
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(dateString) {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
renderMarkdown(content) {
|
||||||
|
if (!content) return ''
|
||||||
|
return md.render(content)
|
||||||
|
},
|
||||||
|
initTheme() {
|
||||||
|
// 从 URL 参数获取主题设置
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const theme = url.searchParams.get('theme')
|
||||||
|
this.isDark = theme === 'dark'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initTheme()
|
||||||
|
this.fetchReleases()
|
||||||
|
}
|
||||||
|
}).mount('#app')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 基础的 Markdown 样式 */
|
||||||
|
.prose {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin: 0.8em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin: 0.6em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-left: 1.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
margin-left: 1.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose code {
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose code {
|
||||||
|
background-color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose code {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre code {
|
||||||
|
display: block;
|
||||||
|
padding: 1em;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a {
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose a {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose blockquote {
|
||||||
|
border-left: 4px solid #e5e7eb;
|
||||||
|
padding-left: 1em;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose blockquote {
|
||||||
|
border-left-color: #374151;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-bg {
|
||||||
|
background-color: #151515;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const { ProxyAgent } = require('undici')
|
||||||
|
const { SocksProxyAgent } = require('socks-proxy-agent')
|
||||||
|
const https = require('https')
|
||||||
|
const fs = require('fs')
|
||||||
|
const { pipeline } = require('stream/promises')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a file from a URL with redirect handling
|
||||||
|
* @param {string} url The URL to download from
|
||||||
|
* @param {string} destinationPath The path to save the file to
|
||||||
|
* @returns {Promise<void>} Promise that resolves when download is complete
|
||||||
|
*/
|
||||||
|
async function downloadWithRedirects(url, destinationPath) {
|
||||||
|
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY
|
||||||
|
if (proxyUrl.startsWith('socks')) {
|
||||||
|
const proxyAgent = new SocksProxyAgent(proxyUrl)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = (url) => {
|
||||||
|
https
|
||||||
|
.get(url, { agent: proxyAgent }, (response) => {
|
||||||
|
if (response.statusCode == 301 || response.statusCode == 302) {
|
||||||
|
request(response.headers.location)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const file = fs.createWriteStream(destinationPath)
|
||||||
|
response.pipe(file)
|
||||||
|
file.on('finish', () => resolve())
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
request(url)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const proxyAgent = new ProxyAgent(proxyUrl)
|
||||||
|
const response = await fetch(url, {
|
||||||
|
dispatcher: proxyAgent
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const file = fs.createWriteStream(destinationPath)
|
||||||
|
await pipeline(response.body, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { downloadWithRedirects }
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const os = require('os')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
const AdmZip = require('adm-zip')
|
||||||
|
const { downloadWithRedirects } = require('./download')
|
||||||
|
|
||||||
|
// Base URL for downloading bun binaries
|
||||||
|
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download'
|
||||||
|
const DEFAULT_BUN_VERSION = '1.2.5' // Default fallback version
|
||||||
|
|
||||||
|
// Mapping of platform+arch to binary package name
|
||||||
|
const BUN_PACKAGES = {
|
||||||
|
'darwin-arm64': 'bun-darwin-aarch64.zip',
|
||||||
|
'darwin-x64': 'bun-darwin-x64.zip',
|
||||||
|
'win32-x64': 'bun-windows-x64.zip',
|
||||||
|
'win32-x64-baseline': 'bun-windows-x64-baseline.zip',
|
||||||
|
'linux-x64': 'bun-linux-x64.zip',
|
||||||
|
'linux-x64-baseline': 'bun-linux-x64-baseline.zip',
|
||||||
|
'linux-arm64': 'bun-linux-aarch64.zip',
|
||||||
|
// MUSL variants
|
||||||
|
'linux-musl-x64': 'bun-linux-x64-musl.zip',
|
||||||
|
'linux-musl-x64-baseline': 'bun-linux-x64-musl-baseline.zip',
|
||||||
|
'linux-musl-arm64': 'bun-linux-aarch64-musl.zip'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads and extracts the bun binary for the specified platform and architecture
|
||||||
|
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
|
||||||
|
* @param {string} arch Architecture to download for (e.g., 'x64', 'arm64')
|
||||||
|
* @param {string} version Version of bun to download
|
||||||
|
* @param {boolean} isMusl Whether to use MUSL variant for Linux
|
||||||
|
* @param {boolean} isBaseline Whether to use baseline variant
|
||||||
|
*/
|
||||||
|
async function downloadBunBinary(platform, arch, version = DEFAULT_BUN_VERSION, isMusl = false, isBaseline = false) {
|
||||||
|
let platformKey = isMusl ? `${platform}-musl-${arch}` : `${platform}-${arch}`
|
||||||
|
if (isBaseline) {
|
||||||
|
platformKey += '-baseline'
|
||||||
|
}
|
||||||
|
const packageName = BUN_PACKAGES[platformKey]
|
||||||
|
|
||||||
|
if (!packageName) {
|
||||||
|
console.error(`No binary available for ${platformKey}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output directory structure
|
||||||
|
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||||
|
// Ensure directories exist
|
||||||
|
fs.mkdirSync(binDir, { recursive: true })
|
||||||
|
|
||||||
|
// Download URL for the specific binary
|
||||||
|
const downloadUrl = `${BUN_RELEASE_BASE_URL}/bun-v${version}/${packageName}`
|
||||||
|
const tempdir = os.tmpdir()
|
||||||
|
// Create a temporary file for the downloaded binary
|
||||||
|
const tempFilename = path.join(tempdir, packageName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Downloading bun ${version} for ${platformKey}...`)
|
||||||
|
console.log(`URL: ${downloadUrl}`)
|
||||||
|
|
||||||
|
// Use the new download function
|
||||||
|
await downloadWithRedirects(downloadUrl, tempFilename)
|
||||||
|
|
||||||
|
// Extract the zip file using adm-zip
|
||||||
|
console.log(`Extracting ${packageName} to ${binDir}...`)
|
||||||
|
const zip = new AdmZip(tempFilename)
|
||||||
|
zip.extractAllTo(tempdir, true)
|
||||||
|
|
||||||
|
// Move files using Node.js fs
|
||||||
|
const sourceDir = path.join(tempdir, packageName.split('.')[0])
|
||||||
|
const files = fs.readdirSync(sourceDir)
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const sourcePath = path.join(sourceDir, file)
|
||||||
|
const destPath = path.join(binDir, file)
|
||||||
|
|
||||||
|
fs.copyFileSync(sourcePath, destPath)
|
||||||
|
fs.unlinkSync(sourcePath)
|
||||||
|
|
||||||
|
// Set executable permissions for non-Windows platforms
|
||||||
|
if (platform !== 'win32') {
|
||||||
|
try {
|
||||||
|
// 755 permission: rwxr-xr-x
|
||||||
|
fs.chmodSync(destPath, '755')
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Warning: Failed to set executable permissions: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
fs.unlinkSync(tempFilename)
|
||||||
|
fs.rmSync(sourceDir, { recursive: true })
|
||||||
|
|
||||||
|
console.log(`Successfully installed bun ${version} for ${platformKey}`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error installing bun for ${platformKey}: ${error.message}`)
|
||||||
|
// Clean up temporary file if it exists
|
||||||
|
if (fs.existsSync(tempFilename)) {
|
||||||
|
fs.unlinkSync(tempFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if binDir is empty and remove it if so
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(binDir)
|
||||||
|
if (files.length === 0) {
|
||||||
|
fs.rmSync(binDir, { recursive: true })
|
||||||
|
console.log(`Removed empty directory: ${binDir}`)
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects current platform and architecture
|
||||||
|
*/
|
||||||
|
function detectPlatformAndArch() {
|
||||||
|
const platform = os.platform()
|
||||||
|
const arch = os.arch()
|
||||||
|
const isMusl = platform === 'linux' && detectIsMusl()
|
||||||
|
const isBaseline = platform === 'win32'
|
||||||
|
|
||||||
|
return { platform, arch, isMusl, isBaseline }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to detect if running on MUSL libc
|
||||||
|
*/
|
||||||
|
function detectIsMusl() {
|
||||||
|
try {
|
||||||
|
// Simple check for Alpine Linux which uses MUSL
|
||||||
|
const output = execSync('cat /etc/os-release').toString()
|
||||||
|
return output.toLowerCase().includes('alpine')
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to install bun
|
||||||
|
*/
|
||||||
|
async function installBun() {
|
||||||
|
// Get the latest version if no specific version is provided
|
||||||
|
const version = DEFAULT_BUN_VERSION
|
||||||
|
console.log(`Using bun version: ${version}`)
|
||||||
|
|
||||||
|
const { platform, arch, isMusl, isBaseline } = detectPlatformAndArch()
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Installing bun ${version} for ${platform}-${arch}${isMusl ? ' (MUSL)' : ''}${isBaseline ? ' (baseline)' : ''}...`
|
||||||
|
)
|
||||||
|
|
||||||
|
await downloadBunBinary(platform, arch, version, isMusl, isBaseline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the installation
|
||||||
|
installBun()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Installation successful')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Installation failed:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const os = require('os')
|
||||||
|
const https = require('https')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
const NODE_VERSION = process.env.NODE_VERSION || '18.18.0' // 默认版本
|
||||||
|
const NODE_RELEASE_BASE_URL = 'https://nodejs.org/dist'
|
||||||
|
|
||||||
|
// 平台映射
|
||||||
|
const NODE_PACKAGES = {
|
||||||
|
'darwin-arm64': `node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
|
||||||
|
'darwin-x64': `node-v${NODE_VERSION}-darwin-x64.tar.gz`,
|
||||||
|
'win32-x64': `node-v${NODE_VERSION}-win32-x64.zip`,
|
||||||
|
'win32-ia32': `node-v${NODE_VERSION}-win32-x86.zip`,
|
||||||
|
'linux-x64': `node-v${NODE_VERSION}-linux-x64.tar.gz`,
|
||||||
|
'linux-arm64': `node-v${NODE_VERSION}-linux-arm64.tar.gz`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数 - 递归复制目录
|
||||||
|
function copyFolderRecursiveSync(source, target) {
|
||||||
|
// 检查目标目录是否存在,不存在则创建
|
||||||
|
if (!fs.existsSync(target)) {
|
||||||
|
fs.mkdirSync(target, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取源目录中的所有文件和文件夹
|
||||||
|
const files = fs.readdirSync(source);
|
||||||
|
|
||||||
|
// 循环处理每个文件/文件夹
|
||||||
|
for (const file of files) {
|
||||||
|
const sourcePath = path.join(source, file);
|
||||||
|
const targetPath = path.join(target, file);
|
||||||
|
|
||||||
|
// 检查是文件还是文件夹
|
||||||
|
if (fs.statSync(sourcePath).isDirectory()) {
|
||||||
|
// 如果是文件夹,递归复制
|
||||||
|
copyFolderRecursiveSync(sourcePath, targetPath);
|
||||||
|
} else {
|
||||||
|
// 如果是文件,直接复制
|
||||||
|
fs.copyFileSync(sourcePath, targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二进制文件存放目录
|
||||||
|
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||||
|
|
||||||
|
// 创建二进制文件存放目录
|
||||||
|
async function createBinariesDir() {
|
||||||
|
if (!fs.existsSync(binariesDir)) {
|
||||||
|
console.log(`Creating binaries directory at ${binariesDir}`)
|
||||||
|
fs.mkdirSync(binariesDir, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前平台对应的包名
|
||||||
|
function getPackageForPlatform() {
|
||||||
|
const platform = os.platform()
|
||||||
|
const arch = os.arch()
|
||||||
|
const key = `${platform}-${arch}`
|
||||||
|
|
||||||
|
console.log(`Current platform: ${platform}, architecture: ${arch}`)
|
||||||
|
|
||||||
|
if (!NODE_PACKAGES[key]) {
|
||||||
|
throw new Error(`Unsupported platform/architecture: ${key}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NODE_PACKAGES[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载 Node.js
|
||||||
|
async function downloadNodeJs() {
|
||||||
|
const packageName = getPackageForPlatform()
|
||||||
|
const downloadUrl = `${NODE_RELEASE_BASE_URL}/v${NODE_VERSION}/${packageName}`
|
||||||
|
const tempFilePath = path.join(os.tmpdir(), packageName)
|
||||||
|
|
||||||
|
console.log(`Downloading Node.js v${NODE_VERSION} from ${downloadUrl}`)
|
||||||
|
console.log(`Temp file path: ${tempFilePath}`)
|
||||||
|
|
||||||
|
// 如果临时文件已存在,先删除
|
||||||
|
if (fs.existsSync(tempFilePath)) {
|
||||||
|
fs.unlinkSync(tempFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const file = fs.createWriteStream(tempFilePath)
|
||||||
|
|
||||||
|
https.get(downloadUrl, (response) => {
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Download started, status code: ${response.statusCode}`)
|
||||||
|
|
||||||
|
response.pipe(file)
|
||||||
|
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close()
|
||||||
|
console.log('Download completed')
|
||||||
|
resolve(tempFilePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
file.on('error', (err) => {
|
||||||
|
fs.unlinkSync(tempFilePath)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
}).on('error', (err) => {
|
||||||
|
if (fs.existsSync(tempFilePath)) {
|
||||||
|
fs.unlinkSync(tempFilePath)
|
||||||
|
}
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解压 Node.js 包
|
||||||
|
async function extractNodeJs(filePath) {
|
||||||
|
const platform = os.platform()
|
||||||
|
const extractDir = path.join(os.tmpdir(), `node-v${NODE_VERSION}-extract`)
|
||||||
|
|
||||||
|
if (fs.existsSync(extractDir)) {
|
||||||
|
console.log(`Removing existing extract directory: ${extractDir}`)
|
||||||
|
fs.rmSync(extractDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Creating extract directory: ${extractDir}`)
|
||||||
|
fs.mkdirSync(extractDir, { recursive: true })
|
||||||
|
|
||||||
|
console.log(`Extracting to ${extractDir}`)
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
// Windows 使用内置的解压工具
|
||||||
|
try {
|
||||||
|
const AdmZip = require('adm-zip')
|
||||||
|
console.log(`Using adm-zip to extract ${filePath}`)
|
||||||
|
const zip = new AdmZip(filePath)
|
||||||
|
zip.extractAllTo(extractDir, true)
|
||||||
|
console.log(`Extraction completed using adm-zip`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error using adm-zip: ${error}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux/Mac 使用 tar
|
||||||
|
try {
|
||||||
|
console.log(`Using tar to extract ${filePath} to ${extractDir}`)
|
||||||
|
execSync(`tar -xzf "${filePath}" -C "${extractDir}"`, { stdio: 'inherit' })
|
||||||
|
console.log(`Extraction completed using tar`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error using tar: ${error}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装 Node.js
|
||||||
|
async function installNodeJs(extractDir) {
|
||||||
|
const platform = os.platform()
|
||||||
|
console.log(`Finding extracted Node.js directory in ${extractDir}`)
|
||||||
|
|
||||||
|
const items = fs.readdirSync(extractDir)
|
||||||
|
console.log(`Found items in extract directory: ${items.join(', ')}`)
|
||||||
|
|
||||||
|
// 找到包含"node-v"的目录名
|
||||||
|
const folderName = items.find(item => item.startsWith('node-v'))
|
||||||
|
|
||||||
|
if (!folderName) {
|
||||||
|
throw new Error(`Could not find Node.js directory in ${extractDir}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found Node.js directory: ${folderName}`)
|
||||||
|
const nodeBinPath = path.join(extractDir, folderName, 'bin')
|
||||||
|
|
||||||
|
console.log(`Node.js bin path: ${nodeBinPath}`)
|
||||||
|
|
||||||
|
// 复制 node 和 npm
|
||||||
|
if (platform === 'win32') {
|
||||||
|
// Windows
|
||||||
|
console.log('Installing Node.js binaries for Windows')
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(extractDir, folderName, 'node.exe'),
|
||||||
|
path.join(binariesDir, 'node.exe')
|
||||||
|
)
|
||||||
|
console.log(`Copied node.exe to ${path.join(binariesDir, 'node.exe')}`)
|
||||||
|
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(extractDir, folderName, 'npm.cmd'),
|
||||||
|
path.join(binariesDir, 'npm.cmd')
|
||||||
|
)
|
||||||
|
console.log(`Copied npm.cmd to ${path.join(binariesDir, 'npm.cmd')}`)
|
||||||
|
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(extractDir, folderName, 'npx.cmd'),
|
||||||
|
path.join(binariesDir, 'npx.cmd')
|
||||||
|
)
|
||||||
|
console.log(`Copied npx.cmd to ${path.join(binariesDir, 'npx.cmd')}`)
|
||||||
|
} else {
|
||||||
|
// Linux/Mac
|
||||||
|
console.log('Installing Node.js binaries for Linux/Mac')
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(nodeBinPath, 'node'),
|
||||||
|
path.join(binariesDir, 'node')
|
||||||
|
)
|
||||||
|
console.log(`Copied node to ${path.join(binariesDir, 'node')}`)
|
||||||
|
|
||||||
|
// 创建npm脚本,指向正确路径
|
||||||
|
const npmScript = `#!/usr/bin/env node
|
||||||
|
require("./node_modules/npm/lib/cli.js")(process)`;
|
||||||
|
fs.writeFileSync(path.join(binariesDir, 'npm'), npmScript);
|
||||||
|
console.log(`Created npm script at ${path.join(binariesDir, 'npm')}`);
|
||||||
|
|
||||||
|
// 创建npx脚本,指向正确路径
|
||||||
|
const npxScript = `#!/usr/bin/env node
|
||||||
|
require("./node_modules/npm/bin/npx-cli.js")`;
|
||||||
|
fs.writeFileSync(path.join(binariesDir, 'npx'), npxScript);
|
||||||
|
console.log(`Created npx script at ${path.join(binariesDir, 'npx')}`);
|
||||||
|
|
||||||
|
// 设置执行权限
|
||||||
|
execSync(`chmod +x "${path.join(binariesDir, 'node')}"`)
|
||||||
|
execSync(`chmod +x "${path.join(binariesDir, 'npm')}"`)
|
||||||
|
execSync(`chmod +x "${path.join(binariesDir, 'npx')}"`)
|
||||||
|
console.log('Set executable permissions for Node.js binaries')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制 npm 相关文件和目录
|
||||||
|
const npmDir = path.join(binariesDir, 'node_modules', 'npm')
|
||||||
|
fs.mkdirSync(npmDir, { recursive: true })
|
||||||
|
console.log(`Created npm directory at ${npmDir}`)
|
||||||
|
|
||||||
|
// 复制 npm 目录的内容
|
||||||
|
const srcNpmDir = path.join(extractDir, folderName, 'lib', 'node_modules', 'npm')
|
||||||
|
console.log(`Copying npm files from ${srcNpmDir} to ${npmDir}`)
|
||||||
|
|
||||||
|
const files = fs.readdirSync(srcNpmDir)
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const srcPath = path.join(srcNpmDir, file)
|
||||||
|
const destPath = path.join(npmDir, file)
|
||||||
|
|
||||||
|
if (fs.lstatSync(srcPath).isDirectory()) {
|
||||||
|
// 使用自定义函数代替fs.cpSync,确保兼容性
|
||||||
|
console.log(`Copying directory: ${file}`)
|
||||||
|
copyFolderRecursiveSync(srcPath, destPath)
|
||||||
|
} else {
|
||||||
|
console.log(`Copying file: ${file}`)
|
||||||
|
fs.copyFileSync(srcPath, destPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Node.js installation completed successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理临时文件
|
||||||
|
async function cleanup(filePath, extractDir) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
console.log(`Cleaning up temp file: ${filePath}`)
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(extractDir)) {
|
||||||
|
console.log(`Cleaning up extract directory: ${extractDir}`)
|
||||||
|
fs.rmSync(extractDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Cleaned up temporary files')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during cleanup:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主安装函数
|
||||||
|
async function install() {
|
||||||
|
try {
|
||||||
|
console.log(`Starting Node.js v${NODE_VERSION} installation...`)
|
||||||
|
|
||||||
|
await createBinariesDir()
|
||||||
|
console.log('Binary directory created/verified')
|
||||||
|
|
||||||
|
const filePath = await downloadNodeJs()
|
||||||
|
console.log(`Downloaded Node.js to ${filePath}`)
|
||||||
|
|
||||||
|
const extractDir = await extractNodeJs(filePath)
|
||||||
|
console.log(`Extracted Node.js to ${extractDir}`)
|
||||||
|
|
||||||
|
await installNodeJs(extractDir)
|
||||||
|
console.log('Installed Node.js binaries')
|
||||||
|
|
||||||
|
await cleanup(filePath, extractDir)
|
||||||
|
console.log('Cleanup completed')
|
||||||
|
|
||||||
|
console.log(`Node.js v${NODE_VERSION} has been installed successfully at ${binariesDir}`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Installation failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行安装
|
||||||
|
install()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Installation process completed successfully')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Fatal error during installation:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const os = require('os')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
const tar = require('tar')
|
||||||
|
const AdmZip = require('adm-zip')
|
||||||
|
const { downloadWithRedirects } = require('./download')
|
||||||
|
|
||||||
|
// Base URL for downloading uv binaries
|
||||||
|
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
|
||||||
|
const DEFAULT_UV_VERSION = '0.6.6'
|
||||||
|
|
||||||
|
// Mapping of platform+arch to binary package name
|
||||||
|
const UV_PACKAGES = {
|
||||||
|
'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz',
|
||||||
|
'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz',
|
||||||
|
'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip',
|
||||||
|
'win32-ia32': 'uv-i686-pc-windows-msvc.zip',
|
||||||
|
'win32-x64': 'uv-x86_64-pc-windows-msvc.zip',
|
||||||
|
'linux-arm64': 'uv-aarch64-unknown-linux-gnu.tar.gz',
|
||||||
|
'linux-ia32': 'uv-i686-unknown-linux-gnu.tar.gz',
|
||||||
|
'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.tar.gz',
|
||||||
|
'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.tar.gz',
|
||||||
|
'linux-s390x': 'uv-s390x-unknown-linux-gnu.tar.gz',
|
||||||
|
'linux-x64': 'uv-x86_64-unknown-linux-gnu.tar.gz',
|
||||||
|
'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.tar.gz',
|
||||||
|
// MUSL variants
|
||||||
|
'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz',
|
||||||
|
'linux-musl-ia32': 'uv-i686-unknown-linux-musl.tar.gz',
|
||||||
|
'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.tar.gz',
|
||||||
|
'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.tar.gz',
|
||||||
|
'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads and extracts the uv binary for the specified platform and architecture
|
||||||
|
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
|
||||||
|
* @param {string} arch Architecture to download for (e.g., 'x64', 'arm64')
|
||||||
|
* @param {string} version Version of uv to download
|
||||||
|
* @param {boolean} isMusl Whether to use MUSL variant for Linux
|
||||||
|
*/
|
||||||
|
async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, isMusl = false) {
|
||||||
|
const platformKey = isMusl ? `${platform}-musl-${arch}` : `${platform}-${arch}`
|
||||||
|
const packageName = UV_PACKAGES[platformKey]
|
||||||
|
|
||||||
|
if (!packageName) {
|
||||||
|
console.error(`No binary available for ${platformKey}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output directory structure
|
||||||
|
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||||
|
// Ensure directories exist
|
||||||
|
fs.mkdirSync(binDir, { recursive: true })
|
||||||
|
|
||||||
|
// Download URL for the specific binary
|
||||||
|
const downloadUrl = `${UV_RELEASE_BASE_URL}/${version}/${packageName}`
|
||||||
|
const tempdir = os.tmpdir()
|
||||||
|
const tempFilename = path.join(tempdir, packageName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Downloading uv ${version} for ${platformKey}...`)
|
||||||
|
console.log(`URL: ${downloadUrl}`)
|
||||||
|
|
||||||
|
await downloadWithRedirects(downloadUrl, tempFilename)
|
||||||
|
|
||||||
|
console.log(`Extracting ${packageName} to ${binDir}...`)
|
||||||
|
|
||||||
|
// 根据文件扩展名选择解压方法
|
||||||
|
if (packageName.endsWith('.zip')) {
|
||||||
|
// 使用 adm-zip 处理 zip 文件
|
||||||
|
const zip = new AdmZip(tempFilename)
|
||||||
|
zip.extractAllTo(binDir, true)
|
||||||
|
fs.unlinkSync(tempFilename)
|
||||||
|
console.log(`Successfully installed uv ${version} for ${platform}-${arch}`)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// tar.gz 文件的处理保持不变
|
||||||
|
await tar.x({
|
||||||
|
file: tempFilename,
|
||||||
|
cwd: tempdir,
|
||||||
|
z: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Move files using Node.js fs
|
||||||
|
const sourceDir = path.join(tempdir, packageName.split('.')[0])
|
||||||
|
const files = fs.readdirSync(sourceDir)
|
||||||
|
for (const file of files) {
|
||||||
|
const sourcePath = path.join(sourceDir, file)
|
||||||
|
const destPath = path.join(binDir, file)
|
||||||
|
fs.copyFileSync(sourcePath, destPath)
|
||||||
|
fs.unlinkSync(sourcePath)
|
||||||
|
|
||||||
|
// Set executable permissions for non-Windows platforms
|
||||||
|
if (platform !== 'win32') {
|
||||||
|
try {
|
||||||
|
fs.chmodSync(destPath, '755')
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Warning: Failed to set executable permissions: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
fs.unlinkSync(tempFilename)
|
||||||
|
fs.rmSync(sourceDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully installed uv ${version} for ${platform}-${arch}`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error installing uv for ${platformKey}: ${error.message}`)
|
||||||
|
|
||||||
|
if (fs.existsSync(tempFilename)) {
|
||||||
|
fs.unlinkSync(tempFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if binDir is empty and remove it if so
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(binDir)
|
||||||
|
if (files.length === 0) {
|
||||||
|
fs.rmSync(binDir, { recursive: true })
|
||||||
|
console.log(`Removed empty directory: ${binDir}`)
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects current platform and architecture
|
||||||
|
*/
|
||||||
|
function detectPlatformAndArch() {
|
||||||
|
const platform = os.platform()
|
||||||
|
const arch = os.arch()
|
||||||
|
const isMusl = platform === 'linux' && detectIsMusl()
|
||||||
|
|
||||||
|
return { platform, arch, isMusl }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to detect if running on MUSL libc
|
||||||
|
*/
|
||||||
|
function detectIsMusl() {
|
||||||
|
try {
|
||||||
|
// Simple check for Alpine Linux which uses MUSL
|
||||||
|
const output = execSync('cat /etc/os-release').toString()
|
||||||
|
return output.toLowerCase().includes('alpine')
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to install uv
|
||||||
|
*/
|
||||||
|
async function installUv() {
|
||||||
|
// Get the latest version if no specific version is provided
|
||||||
|
const version = DEFAULT_UV_VERSION
|
||||||
|
console.log(`Using uv version: ${version}`)
|
||||||
|
|
||||||
|
const { platform, arch, isMusl } = detectPlatformAndArch()
|
||||||
|
|
||||||
|
console.log(`Installing uv ${version} for ${platform}-${arch}${isMusl ? ' (MUSL)' : ''}...`)
|
||||||
|
|
||||||
|
await downloadUvBinary(platform, arch, version, isMusl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the installation
|
||||||
|
installUv()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Installation successful')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Installation failed:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import Cocoa
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class TextSelectionObserver: NSObject {
|
||||||
|
let workspace = NSWorkspace.shared
|
||||||
|
var lastSelectedText: String?
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
// 注册通知观察者
|
||||||
|
let observer = NSWorkspace.shared.notificationCenter
|
||||||
|
observer.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleSelectionChange),
|
||||||
|
name: NSWorkspace.didActivateApplicationNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听选择变化通知
|
||||||
|
var axObserver: AXObserver?
|
||||||
|
let error = AXObserverCreate(getpid(), { observer, element, notification, userData in
|
||||||
|
let selfPointer = userData!.load(as: TextSelectionObserver.self)
|
||||||
|
selfPointer.checkSelectedText()
|
||||||
|
}, &axObserver)
|
||||||
|
|
||||||
|
if error == .success, let axObserver = axObserver {
|
||||||
|
CFRunLoopAddSource(
|
||||||
|
RunLoop.main.getCFRunLoop(),
|
||||||
|
AXObserverGetRunLoopSource(axObserver),
|
||||||
|
.defaultMode
|
||||||
|
)
|
||||||
|
|
||||||
|
// 当前活动应用添加监听
|
||||||
|
updateActiveAppObserver(axObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleSelectionChange(_ notification: Notification) {
|
||||||
|
// 应用切换时更新监听
|
||||||
|
var axObserver: AXObserver?
|
||||||
|
let error = AXObserverCreate(getpid(), { _, _, _, _ in }, &axObserver)
|
||||||
|
if error == .success, let axObserver = axObserver {
|
||||||
|
updateActiveAppObserver(axObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateActiveAppObserver(_ axObserver: AXObserver) {
|
||||||
|
guard let app = workspace.frontmostApplication else { return }
|
||||||
|
let pid = app.processIdentifier
|
||||||
|
let element = AXUIElementCreateApplication(pid)
|
||||||
|
|
||||||
|
// 添加选择变化通知监听
|
||||||
|
AXObserverAddNotification(
|
||||||
|
axObserver,
|
||||||
|
element,
|
||||||
|
kAXSelectedTextChangedNotification as CFString,
|
||||||
|
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSelectedText() {
|
||||||
|
if let text = getSelectedText() {
|
||||||
|
if text.count > 0 && text != lastSelectedText {
|
||||||
|
print(text)
|
||||||
|
fflush(stdout)
|
||||||
|
lastSelectedText = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSelectedText() -> String? {
|
||||||
|
guard let app = NSWorkspace.shared.frontmostApplication else { return nil }
|
||||||
|
let pid = app.processIdentifier
|
||||||
|
|
||||||
|
let axApp = AXUIElementCreateApplication(pid)
|
||||||
|
var focusedElement: AnyObject?
|
||||||
|
|
||||||
|
// Get focused element
|
||||||
|
let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement)
|
||||||
|
guard result == .success else { return nil }
|
||||||
|
|
||||||
|
// Try different approaches to get selected text
|
||||||
|
var selectedText: AnyObject?
|
||||||
|
|
||||||
|
// First try: Direct selected text
|
||||||
|
var textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
|
||||||
|
|
||||||
|
// Second try: Selected text in text area
|
||||||
|
if textResult != .success {
|
||||||
|
var selectedTextRange: AnyObject?
|
||||||
|
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextRangeAttribute as CFString, &selectedTextRange)
|
||||||
|
if textResult == .success {
|
||||||
|
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXValueAttribute as CFString, &selectedText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third try: Get selected text from parent element
|
||||||
|
if textResult != .success {
|
||||||
|
var parent: AnyObject?
|
||||||
|
if AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXParentAttribute as CFString, &parent) == .success {
|
||||||
|
textResult = AXUIElementCopyAttributeValue(parent as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard textResult == .success, let text = selectedText as? String else { return nil }
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let observer = TextSelectionObserver()
|
||||||
|
|
||||||
|
signal(SIGINT) { _ in
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
RunLoop.main.run()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
const { Arch } = require('electron-builder')
|
||||||
|
const { default: removeLocales } = require('./remove-locales')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
exports.default = async function (context) {
|
||||||
|
await removeLocales(context)
|
||||||
|
const platform = context.packager.platform.name
|
||||||
|
const arch = context.arch
|
||||||
|
|
||||||
|
if (platform === 'mac') {
|
||||||
|
const node_modules_path = path.join(
|
||||||
|
context.appOutDir,
|
||||||
|
'Cherry Studio.app',
|
||||||
|
'Contents',
|
||||||
|
'Resources',
|
||||||
|
'app.asar.unpacked',
|
||||||
|
'node_modules'
|
||||||
|
)
|
||||||
|
|
||||||
|
removeDifferentArchNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64'])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === 'linux') {
|
||||||
|
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
|
||||||
|
const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl']
|
||||||
|
removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === 'windows') {
|
||||||
|
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
|
||||||
|
removeDifferentArchNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) {
|
||||||
|
const modulePath = path.join(nodeModulesPath, packageName)
|
||||||
|
const dirs = fs.readdirSync(modulePath)
|
||||||
|
dirs
|
||||||
|
.filter((dir) => !arch.includes(dir))
|
||||||
|
.forEach((dir) => {
|
||||||
|
fs.rmSync(path.join(modulePath, dir), { recursive: true, force: true })
|
||||||
|
console.log(`Removed dir: ${dir}`, arch)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const { downloadNpmPackage } = require('./utils')
|
||||||
|
|
||||||
|
async function downloadNpm(platform) {
|
||||||
|
if (!platform || platform === 'mac') {
|
||||||
|
downloadNpmPackage(
|
||||||
|
'@libsql/darwin-arm64',
|
||||||
|
'https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.4.7.tgz'
|
||||||
|
)
|
||||||
|
downloadNpmPackage('@libsql/darwin-x64', 'https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.4.7.tgz')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!platform || platform === 'linux') {
|
||||||
|
downloadNpmPackage(
|
||||||
|
'@libsql/linux-arm64-gnu',
|
||||||
|
'https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.7.tgz'
|
||||||
|
)
|
||||||
|
downloadNpmPackage(
|
||||||
|
'@libsql/linux-arm64-musl',
|
||||||
|
'https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.7.tgz'
|
||||||
|
)
|
||||||
|
downloadNpmPackage(
|
||||||
|
'@libsql/linux-x64-gnu',
|
||||||
|
'https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.7.tgz'
|
||||||
|
)
|
||||||
|
downloadNpmPackage(
|
||||||
|
'@libsql/linux-x64-musl',
|
||||||
|
'https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!platform || platform === 'windows') {
|
||||||
|
downloadNpmPackage(
|
||||||
|
'@libsql/win32-x64-msvc',
|
||||||
|
'https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformArg = process.argv[2]
|
||||||
|
downloadNpm(platformArg)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
'use strict'
|
||||||
|
Object.defineProperty(exports, '__esModule', { value: true })
|
||||||
|
var fs = require('fs')
|
||||||
|
var path = require('path')
|
||||||
|
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||||
|
var baseLocale = 'zh-CN'
|
||||||
|
var baseFileName = ''.concat(baseLocale, '.json')
|
||||||
|
var baseFilePath = path.join(translationsDir, baseFileName)
|
||||||
|
/**
|
||||||
|
* 递归同步 target 对象,使其与 template 对象保持一致
|
||||||
|
* 1. 如果 template 中存在 target 中缺少的 key,则添加('[to be translated]')
|
||||||
|
* 2. 如果 target 中存在 template 中不存在的 key,则删除
|
||||||
|
* 3. 对于子对象,递归同步
|
||||||
|
*
|
||||||
|
* @param target 目标对象(需要更新的语言对象)
|
||||||
|
* @param template 主模板对象(中文)
|
||||||
|
* @returns 返回是否对 target 进行了更新
|
||||||
|
*/
|
||||||
|
function syncRecursively(target, template) {
|
||||||
|
var isUpdated = false
|
||||||
|
// 添加 template 中存在但 target 中缺少的 key
|
||||||
|
for (var key in template) {
|
||||||
|
if (!(key in target)) {
|
||||||
|
target[key] =
|
||||||
|
typeof template[key] === 'object' && template[key] !== null ? {} : '[to be translated]:'.concat(template[key])
|
||||||
|
console.log('\u6DFB\u52A0\u65B0\u5C5E\u6027\uFF1A'.concat(key))
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
if (typeof template[key] === 'object' && template[key] !== null) {
|
||||||
|
if (typeof target[key] !== 'object' || target[key] === null) {
|
||||||
|
target[key] = {}
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
// 递归同步子对象
|
||||||
|
var childUpdated = syncRecursively(target[key], template[key])
|
||||||
|
if (childUpdated) {
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 删除 target 中存在但 template 中没有的 key
|
||||||
|
for (var targetKey in target) {
|
||||||
|
if (!(targetKey in template)) {
|
||||||
|
console.log('\u79FB\u9664\u591A\u4F59\u5C5E\u6027\uFF1A'.concat(targetKey))
|
||||||
|
delete target[targetKey]
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isUpdated
|
||||||
|
}
|
||||||
|
function syncTranslations() {
|
||||||
|
if (!fs.existsSync(baseFilePath)) {
|
||||||
|
console.error(
|
||||||
|
'\u4E3B\u6A21\u677F\u6587\u4EF6 '.concat(
|
||||||
|
baseFileName,
|
||||||
|
' \u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u6216\u6587\u4EF6\u540D\u3002'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var baseContent = fs.readFileSync(baseFilePath, 'utf-8')
|
||||||
|
var baseJson = {}
|
||||||
|
try {
|
||||||
|
baseJson = JSON.parse(baseContent)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\u89E3\u6790 '.concat(baseFileName, ' \u51FA\u9519:'), error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var files = fs.readdirSync(translationsDir).filter(function (file) {
|
||||||
|
return file.endsWith('.json') && file !== baseFileName
|
||||||
|
})
|
||||||
|
for (var _i = 0, files_1 = files; _i < files_1.length; _i++) {
|
||||||
|
var file = files_1[_i]
|
||||||
|
var filePath = path.join(translationsDir, file)
|
||||||
|
var targetJson = {}
|
||||||
|
try {
|
||||||
|
var fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
targetJson = JSON.parse(fileContent)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'\u89E3\u6790 '.concat(
|
||||||
|
file,
|
||||||
|
' \u51FA\u9519\uFF0C\u8DF3\u8FC7\u6B64\u6587\u4EF6\u3002\u9519\u8BEF\u4FE1\u606F:'
|
||||||
|
),
|
||||||
|
error
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var isUpdated = syncRecursively(targetJson, baseJson)
|
||||||
|
if (isUpdated) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2), 'utf-8')
|
||||||
|
console.log(
|
||||||
|
'\u6587\u4EF6 '.concat(file, ' \u5DF2\u66F4\u65B0\u540C\u6B65\u4E3B\u6A21\u677F\u7684\u5185\u5BB9\u3002')
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\u5199\u5165 '.concat(file, ' \u51FA\u9519:'), error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('\u6587\u4EF6 '.concat(file, ' \u65E0\u9700\u66F4\u65B0\u3002'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncTranslations()
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||||
|
const baseLocale = 'zh-CN'
|
||||||
|
const baseFileName = `${baseLocale}.json`
|
||||||
|
const baseFilePath = path.join(translationsDir, baseFileName)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归同步 target 对象,使其与 template 对象保持一致
|
||||||
|
* 1. 如果 template 中存在 target 中缺少的 key,则添加('[to be translated]')
|
||||||
|
* 2. 如果 target 中存在 template 中不存在的 key,则删除
|
||||||
|
* 3. 对于子对象,递归同步
|
||||||
|
*
|
||||||
|
* @param target 目标对象(需要更新的语言对象)
|
||||||
|
* @param template 主模板对象(中文)
|
||||||
|
* @returns 返回是否对 target 进行了更新
|
||||||
|
*/
|
||||||
|
function syncRecursively(target: any, template: any): boolean {
|
||||||
|
let isUpdated = false
|
||||||
|
|
||||||
|
// 添加 template 中存在但 target 中缺少的 key
|
||||||
|
for (const key in template) {
|
||||||
|
if (!(key in target)) {
|
||||||
|
target[key] =
|
||||||
|
typeof template[key] === 'object' && template[key] !== null ? {} : `[to be translated]:${template[key]}`
|
||||||
|
console.log(`添加新属性:${key}`)
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
if (typeof template[key] === 'object' && template[key] !== null) {
|
||||||
|
if (typeof target[key] !== 'object' || target[key] === null) {
|
||||||
|
target[key] = {}
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
// 递归同步子对象
|
||||||
|
const childUpdated = syncRecursively(target[key], template[key])
|
||||||
|
if (childUpdated) {
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除 target 中存在但 template 中没有的 key
|
||||||
|
for (const targetKey in target) {
|
||||||
|
if (!(targetKey in template)) {
|
||||||
|
console.log(`移除多余属性:${targetKey}`)
|
||||||
|
delete target[targetKey]
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTranslations() {
|
||||||
|
if (!fs.existsSync(baseFilePath)) {
|
||||||
|
console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseContent = fs.readFileSync(baseFilePath, 'utf-8')
|
||||||
|
let baseJson: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
baseJson = JSON.parse(baseContent)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`解析 ${baseFileName} 出错:`, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(translationsDir).filter((file) => file.endsWith('.json') && file !== baseFileName)
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(translationsDir, file)
|
||||||
|
let targetJson: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
targetJson = JSON.parse(fileContent)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`解析 ${file} 出错,跳过此文件。错误信息:`, error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUpdated = syncRecursively(targetJson, baseJson)
|
||||||
|
|
||||||
|
if (isUpdated) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2) + '\n', 'utf-8')
|
||||||
|
console.log(`文件 ${file} 已更新同步主模板的内容`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`写入 ${file} 出错:`, error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`文件 ${file} 无需更新`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTranslations()
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
// 配置信息
|
||||||
|
const config = {
|
||||||
|
R2_CUSTOM_DOMAIN: 'cherrystudio.ocool.online',
|
||||||
|
R2_BUCKET_NAME: 'cherrystudio',
|
||||||
|
// 缓存键名
|
||||||
|
CACHE_KEY: 'cherry-studio-latest-release',
|
||||||
|
VERSION_DB: 'versions.json',
|
||||||
|
LOG_FILE: 'logs.json',
|
||||||
|
MAX_LOGS: 1000 // 最多保存多少条日志
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker 入口函数
|
||||||
|
const worker = {
|
||||||
|
// 定时器触发配置
|
||||||
|
scheduled: {
|
||||||
|
cron: '*/1 * * * *' // 每分钟执行一次
|
||||||
|
},
|
||||||
|
|
||||||
|
// 定时器执行函数 - 只负责检查和更新
|
||||||
|
async scheduled(event, env, ctx) {
|
||||||
|
try {
|
||||||
|
await initDataFiles(env)
|
||||||
|
console.log('开始定时检查新版本...')
|
||||||
|
// 使用新的 checkNewRelease 函数
|
||||||
|
await checkNewRelease(env)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('定时任务执行失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// HTTP 请求处理函数 - 只负责返回数据
|
||||||
|
async fetch(request, env, ctx) {
|
||||||
|
if (!env || !env.R2_BUCKET) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'R2 存储桶未正确配置'
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const filename = url.pathname.slice(1)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 处理文件下载请求
|
||||||
|
if (filename) {
|
||||||
|
return await handleDownload(env, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只返回缓存的版本信息
|
||||||
|
return await getCachedRelease(env)
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default worker
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加日志记录函数
|
||||||
|
*/
|
||||||
|
async function addLog(env, type, event, details = null) {
|
||||||
|
try {
|
||||||
|
const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
|
||||||
|
let logs = { logs: [] }
|
||||||
|
|
||||||
|
if (logFile) {
|
||||||
|
logs = JSON.parse(await logFile.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.logs.unshift({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type,
|
||||||
|
event,
|
||||||
|
details
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保持日志数量在限制内
|
||||||
|
if (logs.logs.length > config.MAX_LOGS) {
|
||||||
|
logs.logs = logs.logs.slice(0, config.MAX_LOGS)
|
||||||
|
}
|
||||||
|
|
||||||
|
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('写入日志失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最新版本信息
|
||||||
|
*/
|
||||||
|
async function getLatestRelease(env) {
|
||||||
|
try {
|
||||||
|
const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
|
||||||
|
if (!cached) {
|
||||||
|
// 如果缓存不存在,先检查版本数据库
|
||||||
|
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||||
|
if (versionDB) {
|
||||||
|
const versions = JSON.parse(await versionDB.text())
|
||||||
|
if (versions.latestVersion) {
|
||||||
|
// 从版本数据库重建缓存
|
||||||
|
const latestVersion = versions.versions[versions.latestVersion]
|
||||||
|
const cacheData = {
|
||||||
|
version: latestVersion.version,
|
||||||
|
publishedAt: latestVersion.publishedAt,
|
||||||
|
changelog: latestVersion.changelog,
|
||||||
|
downloads: latestVersion.files
|
||||||
|
.filter((file) => file.uploaded)
|
||||||
|
.map((file) => ({
|
||||||
|
name: file.name,
|
||||||
|
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||||
|
size: formatFileSize(file.size)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
// 更新缓存
|
||||||
|
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
|
||||||
|
return new Response(JSON.stringify(cacheData), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果版本数据库也没有数据,才执行检查更新
|
||||||
|
const data = await checkNewRelease(env)
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await cached.text()
|
||||||
|
return new Response(data, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
await addLog(env, 'ERROR', '获取版本信息失败', error.message)
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: '获取版本信息失败: ' + error.message,
|
||||||
|
detail: '请稍���再试'
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改下载处理函数,直接接收 env
|
||||||
|
async function handleDownload(env, filename) {
|
||||||
|
try {
|
||||||
|
const object = await env.R2_BUCKET.get(filename)
|
||||||
|
|
||||||
|
if (!object) {
|
||||||
|
return new Response('文件未找到', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
const headers = new Headers()
|
||||||
|
object.writeHttpMetadata(headers)
|
||||||
|
headers.set('etag', object.httpEtag)
|
||||||
|
headers.set('Content-Disposition', `attachment; filename="${filename}"`)
|
||||||
|
|
||||||
|
return new Response(object.body, {
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载文件时发生错误:', error)
|
||||||
|
return new Response('获取文件失败', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件扩展名获取对应的 Content-Type
|
||||||
|
*/
|
||||||
|
function getContentType(filename) {
|
||||||
|
const ext = filename.split('.').pop().toLowerCase()
|
||||||
|
const types = {
|
||||||
|
exe: 'application/x-msdownload', // Windows 可执行文件
|
||||||
|
dmg: 'application/x-apple-diskimage', // macOS 安装包
|
||||||
|
zip: 'application/zip', // 压缩包
|
||||||
|
AppImage: 'application/x-executable', // Linux 可执行文件
|
||||||
|
blockmap: 'application/octet-stream' // 更新文件
|
||||||
|
}
|
||||||
|
return types[ext] || 'application/octet-stream'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小
|
||||||
|
* 将字节转换为人类可读的格式(B, KB, MB, GB)
|
||||||
|
*/
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
|
let size = bytes
|
||||||
|
let unitIndex = 0
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024
|
||||||
|
unitIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号比较函数
|
||||||
|
* 用于对版本号进行排序
|
||||||
|
*/
|
||||||
|
function compareVersions(a, b) {
|
||||||
|
const partsA = a.replace('v', '').split('.')
|
||||||
|
const partsB = b.replace('v', '').split('.')
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||||
|
const numA = parseInt(partsA[i] || 0)
|
||||||
|
const numB = parseInt(partsB[i] || 0)
|
||||||
|
|
||||||
|
if (numA !== numB) {
|
||||||
|
return numA - numB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化数据文件
|
||||||
|
*/
|
||||||
|
async function initDataFiles(env) {
|
||||||
|
try {
|
||||||
|
// 检查并初始化版本数据库
|
||||||
|
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||||
|
if (!versionDB) {
|
||||||
|
const initialVersions = {
|
||||||
|
versions: {},
|
||||||
|
latestVersion: null,
|
||||||
|
lastChecked: new Date().toISOString()
|
||||||
|
}
|
||||||
|
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2))
|
||||||
|
await addLog(env, 'INFO', 'versions.json 初始化成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查并初始化日志文件
|
||||||
|
const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
|
||||||
|
if (!logFile) {
|
||||||
|
const initialLogs = {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'INFO',
|
||||||
|
event: '系统初始化'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2))
|
||||||
|
console.log('logs.json 初始化成功')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化数据文件失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:只获取缓存的版本信息
|
||||||
|
async function getCachedRelease(env) {
|
||||||
|
try {
|
||||||
|
const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
|
||||||
|
if (!cached) {
|
||||||
|
// 如果缓存不存在,从版本数据库获取
|
||||||
|
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||||
|
if (versionDB) {
|
||||||
|
const versions = JSON.parse(await versionDB.text())
|
||||||
|
if (versions.latestVersion) {
|
||||||
|
const latestVersion = versions.versions[versions.latestVersion]
|
||||||
|
const cacheData = {
|
||||||
|
version: latestVersion.version,
|
||||||
|
publishedAt: latestVersion.publishedAt,
|
||||||
|
changelog: latestVersion.changelog,
|
||||||
|
downloads: latestVersion.files
|
||||||
|
.filter((file) => file.uploaded)
|
||||||
|
.map((file) => ({
|
||||||
|
name: file.name,
|
||||||
|
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||||
|
size: formatFileSize(file.size)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
// 重建缓存
|
||||||
|
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
|
||||||
|
return new Response(JSON.stringify(cacheData), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果没有任何数据,返回错误
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: '没有可用的版本信息'
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回缓存数据
|
||||||
|
return new Response(await cached.text(), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:只检查新版本并更新
|
||||||
|
async function checkNewRelease(env) {
|
||||||
|
try {
|
||||||
|
// 获取 GitHub 最新版本
|
||||||
|
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
|
||||||
|
headers: { 'User-Agent': 'CloudflareWorker' }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!githubResponse.ok) {
|
||||||
|
throw new Error('GitHub API 请求失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseData = await githubResponse.json()
|
||||||
|
const version = releaseData.tag_name
|
||||||
|
|
||||||
|
// 获取版本数据库
|
||||||
|
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||||
|
let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() }
|
||||||
|
|
||||||
|
if (versionDB) {
|
||||||
|
versions = JSON.parse(await versionDB.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除版本检查,改为记录是否有文件更新的标志
|
||||||
|
let hasUpdates = false
|
||||||
|
if (versions.latestVersion !== version) {
|
||||||
|
await addLog(env, 'INFO', `发现新版本: ${version}`)
|
||||||
|
hasUpdates = true
|
||||||
|
} else {
|
||||||
|
await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备新版本记录
|
||||||
|
const versionRecord = {
|
||||||
|
version,
|
||||||
|
publishedAt: releaseData.published_at,
|
||||||
|
uploadedAt: null,
|
||||||
|
files: releaseData.assets.map((asset) => ({
|
||||||
|
name: asset.name,
|
||||||
|
size: asset.size,
|
||||||
|
uploaded: false
|
||||||
|
})),
|
||||||
|
changelog: releaseData.body
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查并上传文件
|
||||||
|
for (const asset of releaseData.assets) {
|
||||||
|
try {
|
||||||
|
const existingFile = await env.R2_BUCKET.get(asset.name)
|
||||||
|
// 检查文件是否存在且大小是否一致
|
||||||
|
if (!existingFile || existingFile.size !== asset.size) {
|
||||||
|
hasUpdates = true
|
||||||
|
const response = await fetch(asset.browser_download_url)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`下载失败: HTTP ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await response.arrayBuffer()
|
||||||
|
await env.R2_BUCKET.put(asset.name, file, {
|
||||||
|
httpMetadata: { contentType: getContentType(asset.name) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新文件状态
|
||||||
|
const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name)
|
||||||
|
if (fileIndex !== -1) {
|
||||||
|
versionRecord.files[fileIndex].uploaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(env, 'INFO', `文件${existingFile ? '更新' : '上传'}成功: ${asset.name}`)
|
||||||
|
} else {
|
||||||
|
// 文件存在且大小相同,标记为已上传
|
||||||
|
const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name)
|
||||||
|
if (fileIndex !== -1) {
|
||||||
|
versionRecord.files[fileIndex].uploaded = true
|
||||||
|
}
|
||||||
|
await addLog(env, 'INFO', `文件完整性验证通过: ${asset.name}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await addLog(env, 'ERROR', `文件处理失败: ${asset.name}`, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在有更新或是新版本时才更新数据库和缓存
|
||||||
|
if (hasUpdates) {
|
||||||
|
// 更新版本记录
|
||||||
|
versionRecord.uploadedAt = new Date().toISOString()
|
||||||
|
versions.versions[version] = versionRecord
|
||||||
|
versions.latestVersion = version
|
||||||
|
|
||||||
|
// 保存版本数据库
|
||||||
|
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2))
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
const cacheData = {
|
||||||
|
version,
|
||||||
|
publishedAt: releaseData.published_at,
|
||||||
|
changelog: releaseData.body,
|
||||||
|
downloads: versionRecord.files
|
||||||
|
.filter((file) => file.uploaded)
|
||||||
|
.map((file) => ({
|
||||||
|
name: file.name,
|
||||||
|
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||||
|
size: formatFileSize(file.size)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
|
||||||
|
await addLog(env, 'INFO', hasUpdates ? '更新完成' : '文件完整性检查完成')
|
||||||
|
|
||||||
|
// 清理旧版本
|
||||||
|
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a))
|
||||||
|
if (versionList.length > 2) {
|
||||||
|
// 获取需要保留的两个最新版本
|
||||||
|
const keepVersions = versionList.slice(0, 2)
|
||||||
|
// 获取所有需要删除的版本
|
||||||
|
const oldVersions = versionList.slice(2)
|
||||||
|
|
||||||
|
// 先获取 R2 桶中的所有文件列表
|
||||||
|
const allFiles = await listAllFiles(env)
|
||||||
|
|
||||||
|
// 获取需要保留的文件名列表
|
||||||
|
const keepFiles = new Set()
|
||||||
|
for (const keepVersion of keepVersions) {
|
||||||
|
const versionFiles = versions.versions[keepVersion].files
|
||||||
|
versionFiles.forEach((file) => keepFiles.add(file.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除所有旧版本文件
|
||||||
|
for (const oldVersion of oldVersions) {
|
||||||
|
const oldFiles = versions.versions[oldVersion].files
|
||||||
|
for (const file of oldFiles) {
|
||||||
|
try {
|
||||||
|
if (file.uploaded) {
|
||||||
|
await env.R2_BUCKET.delete(file.name)
|
||||||
|
await addLog(env, 'INFO', `删除旧文件: ${file.name}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await addLog(env, 'ERROR', `删除旧文件失败: ${file.name}`, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete versions.versions[oldVersion]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理可能遗留的旧文件
|
||||||
|
for (const file of allFiles) {
|
||||||
|
if (!keepFiles.has(file.name)) {
|
||||||
|
try {
|
||||||
|
await env.R2_BUCKET.delete(file.name)
|
||||||
|
await addLog(env, 'INFO', `删除遗留文件: ${file.name}`)
|
||||||
|
} catch (error) {
|
||||||
|
await addLog(env, 'ERROR', `删除遗留文件失败: ${file.name}`, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存更新后的版本数据库
|
||||||
|
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await addLog(env, 'INFO', '所有文件完整性检查通过,无需更新')
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUpdates ? cacheData : null
|
||||||
|
} catch (error) {
|
||||||
|
await addLog(env, 'ERROR', '检查新版本失败', error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:获取 R2 桶中的所有文件列表
|
||||||
|
async function listAllFiles(env) {
|
||||||
|
const files = []
|
||||||
|
let cursor
|
||||||
|
|
||||||
|
do {
|
||||||
|
const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] })
|
||||||
|
files.push(...listed.objects)
|
||||||
|
cursor = listed.cursor
|
||||||
|
} while (cursor)
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
exports.default = async function (context) {
|
||||||
|
const platform = context.packager.platform.name
|
||||||
|
|
||||||
|
// 根据平台确定 locales 目录位置
|
||||||
|
let resourceDirs = []
|
||||||
|
if (platform === 'mac') {
|
||||||
|
// macOS 的语言文件位置
|
||||||
|
resourceDirs = [
|
||||||
|
path.join(context.appOutDir, 'Cherry Studio.app', 'Contents', 'Resources'),
|
||||||
|
path.join(
|
||||||
|
context.appOutDir,
|
||||||
|
'Cherry Studio.app',
|
||||||
|
'Contents',
|
||||||
|
'Frameworks',
|
||||||
|
'Electron Framework.framework',
|
||||||
|
'Resources'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
// Windows 和 Linux 的语言文件位置
|
||||||
|
resourceDirs = [path.join(context.appOutDir, 'locales')]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每个资源目录
|
||||||
|
for (const resourceDir of resourceDirs) {
|
||||||
|
if (!fs.existsSync(resourceDir)) {
|
||||||
|
console.log(`Resource directory not found: ${resourceDir}, skipping...`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取所有文件和目录
|
||||||
|
const items = fs.readdirSync(resourceDir)
|
||||||
|
|
||||||
|
// 遍历并删除不需要的语言文件
|
||||||
|
for (const item of items) {
|
||||||
|
if (platform === 'mac') {
|
||||||
|
// 在 macOS 上检查 .lproj 目录
|
||||||
|
if (item.endsWith('.lproj') && !item.match(/^(en|zh|ru)/)) {
|
||||||
|
const dirPath = path.join(resourceDir, item)
|
||||||
|
fs.rmSync(dirPath, { recursive: true, force: true })
|
||||||
|
console.log(`Removed locale directory: ${item} from ${resourceDir}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他平台处理 .pak 文件
|
||||||
|
if (!item.match(/^(en|zh|ru)/)) {
|
||||||
|
const filePath = path.join(resourceDir, item)
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
console.log(`Removed locale file: ${item} from ${resourceDir}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Locale cleanup completed!')
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// replaceSpaces.js
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const directory = 'dist'
|
||||||
|
|
||||||
|
// 处理文件名中的空格
|
||||||
|
function replaceFileNames() {
|
||||||
|
fs.readdir(directory, (err, files) => {
|
||||||
|
if (err) throw err
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const oldPath = path.join(directory, file)
|
||||||
|
const newPath = path.join(directory, file.replace(/ /g, '-'))
|
||||||
|
|
||||||
|
fs.stat(oldPath, (err, stats) => {
|
||||||
|
if (err) throw err
|
||||||
|
|
||||||
|
if (stats.isFile() && oldPath !== newPath) {
|
||||||
|
fs.rename(oldPath, newPath, (err) => {
|
||||||
|
if (err) throw err
|
||||||
|
console.log(`Renamed: ${oldPath} -> ${newPath}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceYmlContent() {
|
||||||
|
fs.readdir(directory, (err, files) => {
|
||||||
|
if (err) throw err
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (path.extname(file).toLowerCase() === '.yml') {
|
||||||
|
const filePath = path.join(directory, file)
|
||||||
|
|
||||||
|
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||||
|
if (err) throw err
|
||||||
|
|
||||||
|
// 替换内容
|
||||||
|
const newContent = data.replace(/Cherry Studio-/g, 'Cherry-Studio-')
|
||||||
|
|
||||||
|
// 写回文件
|
||||||
|
fs.writeFile(filePath, newContent, 'utf8', (err) => {
|
||||||
|
if (err) throw err
|
||||||
|
console.log(`Updated content in: ${filePath}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行两个操作
|
||||||
|
replaceFileNames()
|
||||||
|
replaceYmlContent()
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// replaceSpaces.js
|
|
||||||
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const directory = 'dist'
|
|
||||||
|
|
||||||
fs.readdir(directory, (err, files) => {
|
|
||||||
if (err) throw err
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
const oldPath = path.join(directory, file)
|
|
||||||
const newPath = path.join(directory, file.replace(/ /g, '-'))
|
|
||||||
|
|
||||||
fs.stat(oldPath, (err, stats) => {
|
|
||||||
if (err) throw err
|
|
||||||
|
|
||||||
if (stats.isFile() && oldPath !== newPath) {
|
|
||||||
fs.rename(oldPath, newPath, (err) => {
|
|
||||||
if (err) throw err
|
|
||||||
console.log(`Renamed: ${oldPath} -> ${newPath}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
|
function downloadNpmPackage(packageName, url) {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-download-'))
|
||||||
|
|
||||||
|
const targetDir = path.join('./node_modules/', packageName)
|
||||||
|
const filename = packageName.replace('/', '-') + '.tgz'
|
||||||
|
|
||||||
|
// Skip if directory already exists
|
||||||
|
if (fs.existsSync(targetDir)) {
|
||||||
|
console.log(`${targetDir} already exists, skipping download...`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Downloading ${packageName}...`, url)
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
execSync(`curl --fail -o ${filename} ${url}`)
|
||||||
|
|
||||||
|
console.log(`Extracting ${filename}...`)
|
||||||
|
execSync(`tar -xvf ${filename}`)
|
||||||
|
execSync(`rm -rf ${filename}`)
|
||||||
|
execSync(`mv package ${targetDir}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing ${packageName}: ${error.message}`)
|
||||||
|
if (fs.existsSync(filename)) {
|
||||||
|
fs.unlinkSync(filename)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
downloadNpmPackage
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export interface NodeAppType {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
description?: string
|
||||||
|
author?: string
|
||||||
|
homepage?: string
|
||||||
|
repositoryUrl?: string
|
||||||
|
port?: number
|
||||||
|
installCommand?: string
|
||||||
|
buildCommand?: string
|
||||||
|
startCommand?: string
|
||||||
|
isInstalled: boolean
|
||||||
|
isRunning: boolean
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
@@ -1,33 +1,23 @@
|
|||||||
import fs from 'node:fs'
|
|
||||||
|
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import Store from 'electron-store'
|
|
||||||
import path from 'path'
|
import { getDataPath } from './utils'
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development'
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
isDev && app.setPath('userData', app.getPath('userData') + 'Dev')
|
if (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 DATA_PATH = getDataPath()
|
||||||
|
|
||||||
export const appConfig = new Store()
|
|
||||||
|
|
||||||
export const titleBarOverlayDark = {
|
export const titleBarOverlayDark = {
|
||||||
height: 40,
|
height: 40,
|
||||||
color: '#00000000',
|
color: 'rgba(0,0,0,0)',
|
||||||
symbolColor: '#ffffff'
|
symbolColor: '#ffffff'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const titleBarOverlayLight = {
|
export const titleBarOverlayLight = {
|
||||||
height: 40,
|
height: 40,
|
||||||
color: '#00000000',
|
color: 'rgba(255,255,255,0)',
|
||||||
symbolColor: '#000000'
|
symbolColor: '#000000'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,4 @@
|
|||||||
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
export const isMac = process.platform === 'darwin'
|
||||||
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
export const isWin = process.platform === 'win32'
|
||||||
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
export const isLinux = process.platform === 'linux'
|
||||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
export const isDev = process.env.NODE_ENV === 'development'
|
||||||
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 代码文件
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
declare global {
|
||||||
|
namespace Electron {
|
||||||
|
interface App {
|
||||||
|
isQuitting: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
@@ -1,52 +1,67 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
import { app, BrowserWindow } from 'electron'
|
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
||||||
|
import { app, ipcMain } from 'electron'
|
||||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
|
|
||||||
import { registerIpc } from './ipc'
|
import { registerIpc } from './ipc'
|
||||||
import { updateUserDataPath } from './utils/upgrade'
|
import { registerShortcuts } from './services/ShortcutService'
|
||||||
import { createMainWindow } from './window'
|
import { TrayService } from './services/TrayService'
|
||||||
|
import { windowService } from './services/WindowService'
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// Check for single instance lock
|
||||||
// initialization and is ready to create browser windows.
|
if (!app.requestSingleInstanceLock()) {
|
||||||
// Some APIs can only be used after this event occurs.
|
app.quit()
|
||||||
app.whenReady().then(async () => {
|
process.exit(0)
|
||||||
await updateUserDataPath()
|
} else {
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
|
||||||
// Set app user model id for windows
|
app.whenReady().then(async () => {
|
||||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
// Set app user model id for windows
|
||||||
|
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||||
|
|
||||||
|
const mainWindow = windowService.createMainWindow()
|
||||||
|
new TrayService()
|
||||||
|
|
||||||
|
app.on('activate', function () {
|
||||||
|
const mainWindow = windowService.getMainWindow()
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
|
windowService.createMainWindow()
|
||||||
|
} else {
|
||||||
|
windowService.showMainWindow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
registerShortcuts(mainWindow)
|
||||||
|
|
||||||
|
registerIpc(mainWindow, app)
|
||||||
|
|
||||||
|
replaceDevtoolsFont(mainWindow)
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
installExtension(REDUX_DEVTOOLS)
|
||||||
|
.then((name) => console.log(`Added Extension: ${name}`))
|
||||||
|
.catch((err) => console.log('An error occurred: ', err))
|
||||||
|
}
|
||||||
|
ipcMain.handle('system:getDeviceType', () => {
|
||||||
|
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen for second instance
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
windowService.showMainWindow()
|
||||||
|
})
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
|
||||||
// and ignore CommandOrControl + R in production.
|
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('before-quit', () => {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
app.isQuitting = true
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainWindow = createMainWindow()
|
// 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.
|
||||||
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
|
|
||||||
// for applications and their menu bar to stay active until the user quits
|
|
||||||
// explicitly with Cmd + Q.
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// In this file you can include the rest of your app"s specific main process
|
|
||||||
// code. You can also put them in separate files and require them here.
|
|
||||||
|
|||||||
@@ -1,43 +1,154 @@
|
|||||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
import fs from 'node:fs'
|
||||||
|
|
||||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||||
|
import { MCPServer, Shortcut, ThemeMode } from '@types'
|
||||||
|
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||||
|
import log from 'electron-log'
|
||||||
|
|
||||||
|
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||||
import AppUpdater from './services/AppUpdater'
|
import AppUpdater from './services/AppUpdater'
|
||||||
import BackupManager from './services/BackupManager'
|
import BackupManager from './services/BackupManager'
|
||||||
import FileManager from './services/FileManager'
|
import { configManager } from './services/ConfigManager'
|
||||||
|
import CopilotService from './services/CopilotService'
|
||||||
|
import { ExportService } from './services/ExportService'
|
||||||
|
import FileService from './services/FileService'
|
||||||
|
import FileStorage from './services/FileStorage'
|
||||||
|
import { GeminiService } from './services/GeminiService'
|
||||||
|
import KnowledgeService from './services/KnowledgeService'
|
||||||
|
import MCPService from './services/MCPService'
|
||||||
|
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||||
|
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||||
|
import { TrayService } from './services/TrayService'
|
||||||
|
import { windowService } from './services/WindowService'
|
||||||
|
import { getResourcePath } from './utils'
|
||||||
|
import { decrypt, encrypt } from './utils/aes'
|
||||||
|
import { getFilesDir } from './utils/file'
|
||||||
import { compress, decompress } from './utils/zip'
|
import { compress, decompress } from './utils/zip'
|
||||||
import { createMinappWindow } from './window'
|
import NodeAppService from './services/NodeAppService'
|
||||||
|
|
||||||
const fileManager = new FileManager()
|
const fileManager = new FileStorage()
|
||||||
const backupManager = new BackupManager()
|
const backupManager = new BackupManager()
|
||||||
|
const exportService = new ExportService(fileManager)
|
||||||
|
const mcpService = new MCPService()
|
||||||
|
|
||||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
const appUpdater = new AppUpdater(mainWindow)
|
||||||
|
|
||||||
// IPC
|
ipcMain.handle('app:info', () => ({
|
||||||
ipcMain.handle('get-app-info', () => ({
|
|
||||||
version: app.getVersion(),
|
version: app.getVersion(),
|
||||||
isPackaged: app.isPackaged,
|
isPackaged: app.isPackaged,
|
||||||
appPath: app.getAppPath()
|
appPath: app.getAppPath(),
|
||||||
|
filesPath: getFilesDir(),
|
||||||
|
appDataPath: app.getPath('userData'),
|
||||||
|
resourcesPath: getResourcePath(),
|
||||||
|
logsPath: log.transports.file.getFile().path
|
||||||
}))
|
}))
|
||||||
|
|
||||||
ipcMain.handle('open-website', (_, url: string) => {
|
ipcMain.handle('app:proxy', async (_, proxy: string) => {
|
||||||
shell.openExternal(url)
|
let proxyConfig: ProxyConfig
|
||||||
|
|
||||||
|
if (proxy === 'system') {
|
||||||
|
proxyConfig = { mode: 'system' }
|
||||||
|
} else if (proxy) {
|
||||||
|
proxyConfig = { mode: 'custom', url: proxy }
|
||||||
|
} else {
|
||||||
|
proxyConfig = { mode: 'none' }
|
||||||
|
}
|
||||||
|
|
||||||
|
await proxyManager.configureProxy(proxyConfig)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('set-proxy', (_, proxy: string) => {
|
ipcMain.handle('app:reload', () => mainWindow.reload())
|
||||||
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
|
||||||
|
|
||||||
|
// Update
|
||||||
|
ipcMain.handle('app:show-update-dialog', () => appUpdater.showUpdateDialog(mainWindow))
|
||||||
|
|
||||||
|
// language
|
||||||
|
ipcMain.handle('app:set-language', (_, language) => {
|
||||||
|
configManager.setLanguage(language)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('reload', () => mainWindow.reload())
|
// tray
|
||||||
|
ipcMain.handle('app:set-tray', (_, isActive: boolean) => {
|
||||||
|
configManager.setTray(isActive)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray())
|
||||||
|
|
||||||
|
ipcMain.handle('config:set', (_, key: string, value: any) => {
|
||||||
|
configManager.set(key, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('config:get', (_, key: string) => {
|
||||||
|
return configManager.get(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
// theme
|
||||||
|
ipcMain.handle('app:set-theme', (event, theme: ThemeMode) => {
|
||||||
|
if (theme === configManager.getTheme()) return
|
||||||
|
|
||||||
|
configManager.setTheme(theme)
|
||||||
|
|
||||||
|
// should sync theme change to all windows
|
||||||
|
const senderWindowId = event.sender.id
|
||||||
|
const windows = BrowserWindow.getAllWindows()
|
||||||
|
// 向其他窗口广播主题变化
|
||||||
|
windows.forEach((win) => {
|
||||||
|
if (win.webContents.id !== senderWindowId) {
|
||||||
|
win.webContents.send('theme:change', theme)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow?.setTitleBarOverlay &&
|
||||||
|
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// clear cache
|
||||||
|
ipcMain.handle('app:clear-cache', async () => {
|
||||||
|
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
sessions.map(async (session) => {
|
||||||
|
await session.clearCache()
|
||||||
|
await session.clearStorageData({
|
||||||
|
storages: ['cookies', 'filesystem', 'shadercache', 'websql', 'serviceworkers', 'cachestorage']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
await fileManager.clearTemp()
|
||||||
|
await fs.writeFileSync(log.transports.file.getFile().path, '')
|
||||||
|
return { success: true }
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error('Failed to clear cache:', error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// check for update
|
||||||
|
ipcMain.handle('app:check-for-update', async () => {
|
||||||
|
const update = await appUpdater.autoUpdater.checkForUpdates()
|
||||||
|
return {
|
||||||
|
currentVersion: appUpdater.autoUpdater.currentVersion,
|
||||||
|
updateInfo: update?.updateInfo
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// zip
|
||||||
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
||||||
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
||||||
|
|
||||||
|
// backup
|
||||||
ipcMain.handle('backup:backup', backupManager.backup)
|
ipcMain.handle('backup:backup', backupManager.backup)
|
||||||
ipcMain.handle('backup:restore', backupManager.restore)
|
ipcMain.handle('backup:restore', backupManager.restore)
|
||||||
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||||
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||||
|
ipcMain.handle('backup:listWebdavFiles', backupManager.listWebdavFiles)
|
||||||
|
|
||||||
|
// file
|
||||||
ipcMain.handle('file:open', fileManager.open)
|
ipcMain.handle('file:open', fileManager.open)
|
||||||
|
ipcMain.handle('file:openPath', fileManager.openPath)
|
||||||
ipcMain.handle('file:save', fileManager.save)
|
ipcMain.handle('file:save', fileManager.save)
|
||||||
ipcMain.handle('file:select', fileManager.selectFile)
|
ipcMain.handle('file:select', fileManager.selectFile)
|
||||||
ipcMain.handle('file:upload', fileManager.uploadFile)
|
ipcMain.handle('file:upload', fileManager.uploadFile)
|
||||||
@@ -50,9 +161,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle('file:write', fileManager.writeFile)
|
ipcMain.handle('file:write', fileManager.writeFile)
|
||||||
ipcMain.handle('file:saveImage', fileManager.saveImage)
|
ipcMain.handle('file:saveImage', fileManager.saveImage)
|
||||||
ipcMain.handle('file:base64Image', fileManager.base64Image)
|
ipcMain.handle('file:base64Image', fileManager.base64Image)
|
||||||
|
ipcMain.handle('file:download', fileManager.downloadFile)
|
||||||
|
ipcMain.handle('file:copy', fileManager.copyFile)
|
||||||
|
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
|
||||||
|
|
||||||
|
// fs
|
||||||
|
ipcMain.handle('fs:read', FileService.readFile)
|
||||||
|
|
||||||
|
// minapp
|
||||||
ipcMain.handle('minapp', (_, args) => {
|
ipcMain.handle('minapp', (_, args) => {
|
||||||
createMinappWindow({
|
windowService.createMinappWindow({
|
||||||
url: args.url,
|
url: args.url,
|
||||||
parent: mainWindow,
|
parent: mainWindow,
|
||||||
windowOptions: {
|
windowOptions: {
|
||||||
@@ -62,17 +180,161 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
// export
|
||||||
appConfig.set('theme', theme)
|
ipcMain.handle('export:word', exportService.exportToWord)
|
||||||
mainWindow?.setTitleBarOverlay &&
|
|
||||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
// open path
|
||||||
|
ipcMain.handle('open:path', async (_, path: string) => {
|
||||||
|
await shell.openPath(path)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
// shortcuts
|
||||||
ipcMain.handle('check-for-update', async () => {
|
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
|
||||||
return {
|
configManager.setShortcuts(shortcuts)
|
||||||
currentVersion: autoUpdater.currentVersion,
|
// Refresh shortcuts registration
|
||||||
update: await autoUpdater.checkForUpdates()
|
if (mainWindow) {
|
||||||
|
unregisterAllShortcuts()
|
||||||
|
registerShortcuts(mainWindow)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// knowledge base
|
||||||
|
ipcMain.handle('knowledge-base:create', KnowledgeService.create)
|
||||||
|
ipcMain.handle('knowledge-base:reset', KnowledgeService.reset)
|
||||||
|
ipcMain.handle('knowledge-base:delete', KnowledgeService.delete)
|
||||||
|
ipcMain.handle('knowledge-base:add', KnowledgeService.add)
|
||||||
|
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
|
||||||
|
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
|
||||||
|
ipcMain.handle('knowledge-base:rerank', KnowledgeService.rerank)
|
||||||
|
|
||||||
|
// window
|
||||||
|
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
|
||||||
|
mainWindow?.setMinimumSize(width, height)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('window:reset-minimum-size', () => {
|
||||||
|
mainWindow?.setMinimumSize(1080, 600)
|
||||||
|
const [width, height] = mainWindow?.getSize() ?? [1080, 600]
|
||||||
|
if (width < 1080) {
|
||||||
|
mainWindow?.setSize(1080, height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// gemini
|
||||||
|
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile)
|
||||||
|
ipcMain.handle('gemini:base64-file', GeminiService.base64File)
|
||||||
|
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile)
|
||||||
|
ipcMain.handle('gemini:list-files', GeminiService.listFiles)
|
||||||
|
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile)
|
||||||
|
|
||||||
|
// mini window
|
||||||
|
ipcMain.handle('miniwindow:show', () => windowService.showMiniWindow())
|
||||||
|
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
|
||||||
|
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
|
||||||
|
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
|
||||||
|
|
||||||
|
// aes
|
||||||
|
ipcMain.handle('aes:encrypt', (_, text: string, secretKey: string, iv: string) => encrypt(text, secretKey, iv))
|
||||||
|
ipcMain.handle('aes:decrypt', (_, encryptedData: string, iv: string, secretKey: string) =>
|
||||||
|
decrypt(encryptedData, iv, secretKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register MCP handlers
|
||||||
|
ipcMain.on('mcp:servers-from-renderer', (_, servers) => mcpService.setServers(servers))
|
||||||
|
ipcMain.handle('mcp:list-servers', async () => mcpService.listAvailableServices())
|
||||||
|
ipcMain.handle('mcp:add-server', async (_, server: MCPServer) => mcpService.addServer(server))
|
||||||
|
ipcMain.handle('mcp:update-server', async (_, server: MCPServer) => mcpService.updateServer(server))
|
||||||
|
ipcMain.handle('mcp:delete-server', async (_, serverName: string) => mcpService.deleteServer(serverName))
|
||||||
|
ipcMain.handle('mcp:set-server-active', async (_, { name, isActive }) =>
|
||||||
|
mcpService.setServerActive({ name, isActive })
|
||||||
|
)
|
||||||
|
|
||||||
|
// According to preload, this should take no parameters, but our implementation accepts
|
||||||
|
// an optional serverName for better flexibility
|
||||||
|
ipcMain.handle('mcp:list-tools', async (_, serverName?: string) => mcpService.listTools(serverName))
|
||||||
|
ipcMain.handle('mcp:call-tool', async (_, params: { client: string; name: string; args: any }) =>
|
||||||
|
mcpService.callTool(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
|
||||||
|
|
||||||
|
// Shell API
|
||||||
|
ipcMain.handle('shell:openExternal', async (_, url: string) => {
|
||||||
|
try {
|
||||||
|
log.info(`Opening external URL: ${url}`)
|
||||||
|
return await shell.openExternal(url)
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error opening external URL:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
|
||||||
|
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
|
||||||
|
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
|
||||||
|
ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js'))
|
||||||
|
|
||||||
|
// Listen for changes in MCP servers and notify renderer
|
||||||
|
mcpService.on('servers-updated', (servers) => {
|
||||||
|
mainWindow?.webContents.send('mcp:servers-updated', servers)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('before-quit', () => mcpService.cleanup())
|
||||||
|
|
||||||
|
//copilot
|
||||||
|
ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage)
|
||||||
|
ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken)
|
||||||
|
ipcMain.handle('copilot:save-copilot-token', CopilotService.saveCopilotToken)
|
||||||
|
ipcMain.handle('copilot:get-token', CopilotService.getToken)
|
||||||
|
ipcMain.handle('copilot:logout', CopilotService.logout)
|
||||||
|
ipcMain.handle('copilot:get-user', CopilotService.getUser)
|
||||||
|
|
||||||
|
// Node app management
|
||||||
|
const nodeAppService = NodeAppService.getInstance()
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:list', async () => await nodeAppService.getAllApps())
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:add', async (_, app) => await nodeAppService.addApp(app))
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:install', async (_, appId) => await nodeAppService.installApp(appId))
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:update', async (_, appId) => await nodeAppService.updateApp(appId))
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:start', async (_, appId) => await nodeAppService.startApp(appId))
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:stop', async (_, appId) => await nodeAppService.stopApp(appId))
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:uninstall', async (_, appId) => await nodeAppService.uninstallApp(appId))
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:deploy-zip', async (_, zipPath, options) => await nodeAppService.deployFromZip(zipPath, options))
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:deploy-git', async (_, repoUrl, options) => await nodeAppService.deployFromGit(repoUrl, options))
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:check-node', async () => {
|
||||||
|
const isNodeInstalled = await isBinaryExists('node')
|
||||||
|
return isNodeInstalled
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('nodeapp:install-node', async () => {
|
||||||
|
return await nodeAppService.installNodeJs()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen for changes in Node.js apps and notify renderer
|
||||||
|
nodeAppService.on('apps-updated', (apps) => {
|
||||||
|
mainWindow?.webContents.send('nodeapp:updated', apps)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('before-quit', () => nodeAppService.cleanup())
|
||||||
|
|
||||||
|
// 运行简单命令
|
||||||
|
ipcMain.handle('app:run-command', async (_, command: string) => {
|
||||||
|
try {
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
const result = execSync(command).toString()
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error running command:', error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as fs from 'node:fs'
|
||||||
|
|
||||||
|
import { JsonLoader } from '@llm-tools/embedjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drafts 应用导出的笔记文件加载器
|
||||||
|
* 原始文件是一个 JSON 数组。每条笔记只保留 content、tags、modified_at 三个字段
|
||||||
|
*/
|
||||||
|
export class DraftsExportLoader extends JsonLoader {
|
||||||
|
constructor(filePath: string) {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
const rawJson = JSON.parse(fileContent) as any[]
|
||||||
|
const json = rawJson.map((item) => {
|
||||||
|
return {
|
||||||
|
content: item.content?.replace(/\n/g, '<br>'),
|
||||||
|
tags: item.tags,
|
||||||
|
modified_at: item.created_at
|
||||||
|
}
|
||||||
|
})
|
||||||
|
super({ object: json })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
|
||||||
|
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { cleanString } from '@llm-tools/embedjs-utils'
|
||||||
|
import { getTempDir } from '@main/utils/file'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
import EPub from 'epub'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* epub 加载器的配置选项
|
||||||
|
*/
|
||||||
|
interface EpubLoaderOptions {
|
||||||
|
/** epub 文件路径 */
|
||||||
|
filePath: string
|
||||||
|
/** 文本分块大小 */
|
||||||
|
chunkSize: number
|
||||||
|
/** 分块重叠大小 */
|
||||||
|
chunkOverlap: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* epub 文件的元数据信息
|
||||||
|
*/
|
||||||
|
interface EpubMetadata {
|
||||||
|
/** 作者显示名称(例如:"Lewis Carroll") */
|
||||||
|
creator?: string
|
||||||
|
/** 作者规范化名称,用于排序和索引(例如:"Carroll, Lewis") */
|
||||||
|
creatorFileAs?: string
|
||||||
|
/** 书籍标题(例如:"Alice's Adventures in Wonderland") */
|
||||||
|
title?: string
|
||||||
|
/** 语言代码(例如:"en" 或 "zh-CN") */
|
||||||
|
language?: string
|
||||||
|
/** 主题或分类(例如:"Fantasy"、"Fiction") */
|
||||||
|
subject?: string
|
||||||
|
/** 创建日期(例如:"2024-02-14") */
|
||||||
|
date?: string
|
||||||
|
/** 书籍描述或简介 */
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* epub 章节信息
|
||||||
|
*/
|
||||||
|
interface EpubChapter {
|
||||||
|
/** 章节 ID */
|
||||||
|
id: string
|
||||||
|
/** 章节标题 */
|
||||||
|
title?: string
|
||||||
|
/** 章节顺序 */
|
||||||
|
order?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* epub 文件加载器
|
||||||
|
* 用于解析 epub 电子书文件,提取文本内容和元数据
|
||||||
|
*/
|
||||||
|
export class EpubLoader extends BaseLoader<Record<string, string | number | boolean>, Record<string, unknown>> {
|
||||||
|
protected filePath: string
|
||||||
|
protected chunkSize: number
|
||||||
|
protected chunkOverlap: number
|
||||||
|
private extractedText: string
|
||||||
|
private metadata: EpubMetadata | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 epub 加载器实例
|
||||||
|
* @param options 加载器配置选项
|
||||||
|
*/
|
||||||
|
constructor(options: EpubLoaderOptions) {
|
||||||
|
super(options.filePath, {
|
||||||
|
chunkSize: options.chunkSize,
|
||||||
|
chunkOverlap: options.chunkOverlap
|
||||||
|
})
|
||||||
|
this.filePath = options.filePath
|
||||||
|
this.chunkSize = options.chunkSize
|
||||||
|
this.chunkOverlap = options.chunkOverlap
|
||||||
|
this.extractedText = ''
|
||||||
|
this.metadata = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待 epub 文件初始化完成
|
||||||
|
* epub 库使用事件机制,需要等待 'end' 事件触发后才能访问文件内容
|
||||||
|
* @param epub epub 实例
|
||||||
|
* @returns 元数据和章节信息
|
||||||
|
*/
|
||||||
|
private waitForEpubInit(epub: any): Promise<{ metadata: EpubMetadata; chapters: EpubChapter[] }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
epub.on('end', () => {
|
||||||
|
// 提取元数据
|
||||||
|
const metadata: EpubMetadata = {
|
||||||
|
creator: epub.metadata.creator,
|
||||||
|
creatorFileAs: epub.metadata.creatorFileAs,
|
||||||
|
title: epub.metadata.title,
|
||||||
|
language: epub.metadata.language,
|
||||||
|
subject: epub.metadata.subject,
|
||||||
|
date: epub.metadata.date,
|
||||||
|
description: epub.metadata.description
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取章节信息
|
||||||
|
const chapters: EpubChapter[] = epub.flow.map((chapter: any, index: number) => ({
|
||||||
|
id: chapter.id,
|
||||||
|
title: chapter.title || `Chapter ${index + 1}`,
|
||||||
|
order: index + 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
resolve({ metadata, chapters })
|
||||||
|
})
|
||||||
|
|
||||||
|
epub.on('error', (error: Error) => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
epub.parse()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取章节内容
|
||||||
|
* @param epub epub 实例
|
||||||
|
* @param chapterId 章节 ID
|
||||||
|
* @returns 章节文本内容
|
||||||
|
*/
|
||||||
|
private getChapter(epub: any, chapterId: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
epub.getChapter(chapterId, (error: Error | null, text: string) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
|
resolve(text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 epub 文件中提取文本内容
|
||||||
|
* 1. 检查文件是否存在
|
||||||
|
* 2. 初始化 epub 并获取元数据
|
||||||
|
* 3. 遍历所有章节并提取文本
|
||||||
|
* 4. 清理 HTML 标签
|
||||||
|
* 5. 合并所有章节文本
|
||||||
|
*/
|
||||||
|
private async extractTextFromEpub() {
|
||||||
|
try {
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(this.filePath)) {
|
||||||
|
throw new Error(`File not found: ${this.filePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const epub = new EPub(this.filePath)
|
||||||
|
|
||||||
|
// 等待 epub 初始化完成并获取元数据
|
||||||
|
const { metadata, chapters } = await this.waitForEpubInit(epub)
|
||||||
|
this.metadata = metadata
|
||||||
|
|
||||||
|
if (!epub.flow || epub.flow.length === 0) {
|
||||||
|
throw new Error('No content found in epub file')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用临时文件而不是内存数组
|
||||||
|
const tempFilePath = path.join(getTempDir(), `epub-${Date.now()}.txt`)
|
||||||
|
const writeStream = fs.createWriteStream(tempFilePath)
|
||||||
|
|
||||||
|
// 遍历所有章节
|
||||||
|
for (const chapter of chapters) {
|
||||||
|
try {
|
||||||
|
const content = await this.getChapter(epub, chapter.id)
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 HTML 标签并清理文本
|
||||||
|
const text = content
|
||||||
|
.replace(/<[^>]*>/g, ' ') // 移除所有 HTML 标签
|
||||||
|
.replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格
|
||||||
|
.trim() // 移除首尾空白
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
// 直接写入文件
|
||||||
|
writeStream.write(text + '\n\n')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[EpubLoader] Error processing chapter ${chapter.id}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭写入流
|
||||||
|
writeStream.end()
|
||||||
|
|
||||||
|
// 等待写入完成
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
writeStream.on('finish', resolve)
|
||||||
|
writeStream.on('error', reject)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从临时文件读取内容
|
||||||
|
this.extractedText = fs.readFileSync(tempFilePath, 'utf-8')
|
||||||
|
|
||||||
|
// 删除临时文件
|
||||||
|
fs.unlinkSync(tempFilePath)
|
||||||
|
|
||||||
|
// 只添加一条完成日志
|
||||||
|
Logger.info(`[EpubLoader] 电子书 ${this.metadata?.title || path.basename(this.filePath)} 处理完成`)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[EpubLoader] Error in extractTextFromEpub:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文本块
|
||||||
|
* 重写 BaseLoader 的方法,将提取的文本分割成适当大小的块
|
||||||
|
* 每个块都包含源文件和元数据信息
|
||||||
|
*/
|
||||||
|
override async *getUnfilteredChunks() {
|
||||||
|
// 如果还没有提取文本,先提取
|
||||||
|
if (!this.extractedText) {
|
||||||
|
await this.extractTextFromEpub()
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info('[EpubLoader] 书名:', this.metadata?.title || '未知书名', ' 文本大小:', this.extractedText.length)
|
||||||
|
|
||||||
|
// 创建文本分块器
|
||||||
|
const chunker = new RecursiveCharacterTextSplitter({
|
||||||
|
chunkSize: this.chunkSize,
|
||||||
|
chunkOverlap: this.chunkOverlap
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理并分割文本
|
||||||
|
const chunks = await chunker.splitText(cleanString(this.extractedText))
|
||||||
|
|
||||||
|
// 为每个文本块添加元数据
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
yield {
|
||||||
|
pageContent: chunk,
|
||||||
|
metadata: {
|
||||||
|
source: this.filePath,
|
||||||
|
title: this.metadata?.title || '',
|
||||||
|
creator: this.metadata?.creator || '',
|
||||||
|
language: this.metadata?.language || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import * as fs from 'node:fs'
|
||||||
|
|
||||||
|
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
||||||
|
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||||
|
import { LoaderReturn } from '@shared/config/types'
|
||||||
|
import { FileType, KnowledgeBaseParams } from '@types'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
|
import { DraftsExportLoader } from './draftsExportLoader'
|
||||||
|
import { EpubLoader } from './epubLoader'
|
||||||
|
import { OdLoader, OdType } from './odLoader'
|
||||||
|
|
||||||
|
// 文件扩展名到加载器类型的映射
|
||||||
|
const FILE_LOADER_MAP: Record<string, string> = {
|
||||||
|
// 内置类型
|
||||||
|
'.pdf': 'common',
|
||||||
|
'.csv': 'common',
|
||||||
|
'.docx': 'common',
|
||||||
|
'.pptx': 'common',
|
||||||
|
'.xlsx': 'common',
|
||||||
|
'.md': 'common',
|
||||||
|
// OD类型
|
||||||
|
'.odt': 'od',
|
||||||
|
'.ods': 'od',
|
||||||
|
'.odp': 'od',
|
||||||
|
// epub类型
|
||||||
|
'.epub': 'epub',
|
||||||
|
// Drafts类型
|
||||||
|
'.draftsexport': 'drafts',
|
||||||
|
// HTML类型
|
||||||
|
'.html': 'html',
|
||||||
|
'.htm': 'html',
|
||||||
|
// JSON类型
|
||||||
|
'.json': 'json'
|
||||||
|
// 其他类型默认为文本类型
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addOdLoader(
|
||||||
|
ragApplication: RAGApplication,
|
||||||
|
file: FileType,
|
||||||
|
base: KnowledgeBaseParams,
|
||||||
|
forceReload: boolean
|
||||||
|
): Promise<AddLoaderReturn> {
|
||||||
|
const loaderMap: Record<string, OdType> = {
|
||||||
|
'.odt': OdType.OdtLoader,
|
||||||
|
'.ods': OdType.OdsLoader,
|
||||||
|
'.odp': OdType.OdpLoader
|
||||||
|
}
|
||||||
|
const odType = loaderMap[file.ext]
|
||||||
|
if (!odType) {
|
||||||
|
throw new Error('Unknown odType')
|
||||||
|
}
|
||||||
|
return ragApplication.addLoader(
|
||||||
|
new OdLoader({
|
||||||
|
odType,
|
||||||
|
filePath: file.path,
|
||||||
|
chunkSize: base.chunkSize,
|
||||||
|
chunkOverlap: base.chunkOverlap
|
||||||
|
}) as any,
|
||||||
|
forceReload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addFileLoader(
|
||||||
|
ragApplication: RAGApplication,
|
||||||
|
file: FileType,
|
||||||
|
base: KnowledgeBaseParams,
|
||||||
|
forceReload: boolean
|
||||||
|
): Promise<LoaderReturn> {
|
||||||
|
// 获取文件类型,如果没有匹配则默认为文本类型
|
||||||
|
const loaderType = FILE_LOADER_MAP[file.ext.toLowerCase()] || 'text'
|
||||||
|
let loaderReturn: AddLoaderReturn
|
||||||
|
|
||||||
|
// JSON类型处理
|
||||||
|
let jsonObject = {}
|
||||||
|
let jsonParsed = true
|
||||||
|
Logger.info(`[KnowledgeBase] processing file ${file.path} as ${loaderType} type`)
|
||||||
|
switch (loaderType) {
|
||||||
|
case 'common':
|
||||||
|
// 内置类型处理
|
||||||
|
loaderReturn = await ragApplication.addLoader(
|
||||||
|
new LocalPathLoader({
|
||||||
|
path: file.path,
|
||||||
|
chunkSize: base.chunkSize,
|
||||||
|
chunkOverlap: base.chunkOverlap
|
||||||
|
}) as any,
|
||||||
|
forceReload
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'od':
|
||||||
|
// OD类型处理
|
||||||
|
loaderReturn = await addOdLoader(ragApplication, file, base, forceReload)
|
||||||
|
break
|
||||||
|
case 'epub':
|
||||||
|
// epub类型处理
|
||||||
|
loaderReturn = await ragApplication.addLoader(
|
||||||
|
new EpubLoader({
|
||||||
|
filePath: file.path,
|
||||||
|
chunkSize: base.chunkSize ?? 1000,
|
||||||
|
chunkOverlap: base.chunkOverlap ?? 200
|
||||||
|
}) as any,
|
||||||
|
forceReload
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'drafts':
|
||||||
|
// Drafts类型处理
|
||||||
|
loaderReturn = await ragApplication.addLoader(new DraftsExportLoader(file.path) as any, forceReload)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'html':
|
||||||
|
// HTML类型处理
|
||||||
|
loaderReturn = await ragApplication.addLoader(
|
||||||
|
new WebLoader({
|
||||||
|
urlOrContent: fs.readFileSync(file.path, 'utf-8'),
|
||||||
|
chunkSize: base.chunkSize,
|
||||||
|
chunkOverlap: base.chunkOverlap
|
||||||
|
}) as any,
|
||||||
|
forceReload
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'json':
|
||||||
|
try {
|
||||||
|
jsonObject = JSON.parse(fs.readFileSync(file.path, 'utf-8'))
|
||||||
|
} catch (error) {
|
||||||
|
jsonParsed = false
|
||||||
|
Logger.warn('[KnowledgeBase] failed parsing json file, falling back to text processing:', file.path, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonParsed) {
|
||||||
|
loaderReturn = await ragApplication.addLoader(new JsonLoader({ object: jsonObject }), forceReload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// fallthrough - JSON 解析失败时作为文本处理
|
||||||
|
default:
|
||||||
|
// 文本类型处理(默认)
|
||||||
|
// 如果是其他文本类型且尚未读取文件,则读取文件
|
||||||
|
loaderReturn = await ragApplication.addLoader(
|
||||||
|
new TextLoader({
|
||||||
|
text: fs.readFileSync(file.path, 'utf-8'),
|
||||||
|
chunkSize: base.chunkSize,
|
||||||
|
chunkOverlap: base.chunkOverlap
|
||||||
|
}) as any,
|
||||||
|
forceReload
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
entriesAdded: loaderReturn.entriesAdded,
|
||||||
|
uniqueId: loaderReturn.uniqueId,
|
||||||
|
uniqueIds: [loaderReturn.uniqueId],
|
||||||
|
loaderType: loaderReturn.loaderType
|
||||||
|
} as LoaderReturn
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
|
||||||
|
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { cleanString } from '@llm-tools/embedjs-utils'
|
||||||
|
import md5 from 'md5'
|
||||||
|
import { OfficeParserConfig, parseOfficeAsync } from 'officeparser'
|
||||||
|
|
||||||
|
export enum OdType {
|
||||||
|
OdtLoader = 'OdtLoader',
|
||||||
|
OdsLoader = 'OdsLoader',
|
||||||
|
OdpLoader = 'OdpLoader',
|
||||||
|
undefined = 'undefined'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OdLoader<OdType> extends BaseLoader<{ type: string }> {
|
||||||
|
private readonly odType: OdType
|
||||||
|
private readonly filePath: string
|
||||||
|
private extractedText: string
|
||||||
|
private config: OfficeParserConfig
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
odType,
|
||||||
|
filePath,
|
||||||
|
chunkSize,
|
||||||
|
chunkOverlap
|
||||||
|
}: {
|
||||||
|
odType: OdType
|
||||||
|
filePath: string
|
||||||
|
chunkSize?: number
|
||||||
|
chunkOverlap?: number
|
||||||
|
}) {
|
||||||
|
super(`${odType}_${md5(filePath)}`, { filePath }, chunkSize ?? 1000, chunkOverlap ?? 0)
|
||||||
|
this.odType = odType
|
||||||
|
this.filePath = filePath
|
||||||
|
this.extractedText = ''
|
||||||
|
this.config = {
|
||||||
|
newlineDelimiter: ' ',
|
||||||
|
ignoreNotes: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractTextFromOdt() {
|
||||||
|
try {
|
||||||
|
this.extractedText = await parseOfficeAsync(this.filePath, this.config)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('odLoader error', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override async *getUnfilteredChunks() {
|
||||||
|
if (!this.extractedText) {
|
||||||
|
await this.extractTextFromOdt()
|
||||||
|
}
|
||||||
|
const chunker = new RecursiveCharacterTextSplitter({
|
||||||
|
chunkSize: this.chunkSize,
|
||||||
|
chunkOverlap: this.chunkOverlap
|
||||||
|
})
|
||||||
|
|
||||||
|
const chunks = await chunker.splitText(cleanString(this.extractedText))
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
yield {
|
||||||
|
pageContent: chunk,
|
||||||
|
metadata: {
|
||||||
|
type: this.odType as string,
|
||||||
|
source: this.filePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { KnowledgeBaseParams } from '@types'
|
||||||
|
|
||||||
|
export default abstract class BaseReranker {
|
||||||
|
protected base: KnowledgeBaseParams
|
||||||
|
constructor(base: KnowledgeBaseParams) {
|
||||||
|
if (!base.rerankModel) {
|
||||||
|
throw new Error('Rerank model is required')
|
||||||
|
}
|
||||||
|
this.base = base
|
||||||
|
}
|
||||||
|
abstract rerank(query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]>
|
||||||
|
|
||||||
|
public defaultHeaders() {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${this.base.rerankApiKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { KnowledgeBaseParams } from '@types'
|
||||||
|
|
||||||
|
import BaseReranker from './BaseReranker'
|
||||||
|
|
||||||
|
export default class DefaultReranker extends BaseReranker {
|
||||||
|
constructor(base: KnowledgeBaseParams) {
|
||||||
|
super(base)
|
||||||
|
}
|
||||||
|
async rerank(): Promise<ExtractChunkData[]> {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { KnowledgeBaseParams } from '@types'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import BaseReranker from './BaseReranker'
|
||||||
|
|
||||||
|
export default class JinaReranker extends BaseReranker {
|
||||||
|
constructor(base: KnowledgeBaseParams) {
|
||||||
|
super(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
|
||||||
|
const baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||||
|
? this.base.rerankBaseURL.slice(0, -1)
|
||||||
|
: this.base.rerankBaseURL
|
||||||
|
const url = `${baseURL}/rerank`
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
model: this.base.rerankModel,
|
||||||
|
query,
|
||||||
|
documents: searchResults.map((doc) => doc.pageContent),
|
||||||
|
top_n: this.base.topN
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() })
|
||||||
|
|
||||||
|
const rerankResults = data.results
|
||||||
|
console.log(rerankResults)
|
||||||
|
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
|
||||||
|
return searchResults
|
||||||
|
.map((doc: ExtractChunkData, index: number) => {
|
||||||
|
const score = resultMap.get(index)
|
||||||
|
if (score === undefined) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
score
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Jina Reranker API 错误:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { KnowledgeBaseParams } from '@types'
|
||||||
|
|
||||||
|
import BaseReranker from './BaseReranker'
|
||||||
|
import RerankerFactory from './RerankerFactory'
|
||||||
|
|
||||||
|
export default class Reranker {
|
||||||
|
private sdk: BaseReranker
|
||||||
|
constructor(base: KnowledgeBaseParams) {
|
||||||
|
this.sdk = RerankerFactory.create(base)
|
||||||
|
}
|
||||||
|
public async rerank(query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> {
|
||||||
|
return this.sdk.rerank(query, searchResults)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { KnowledgeBaseParams } from '@types'
|
||||||
|
|
||||||
|
import BaseReranker from './BaseReranker'
|
||||||
|
import DefaultReranker from './DefaultReranker'
|
||||||
|
import JinaReranker from './JinaReranker'
|
||||||
|
import SiliconFlowReranker from './SiliconFlowReranker'
|
||||||
|
|
||||||
|
export default class RerankerFactory {
|
||||||
|
static create(base: KnowledgeBaseParams): BaseReranker {
|
||||||
|
if (base.rerankModelProvider === 'silicon') {
|
||||||
|
return new SiliconFlowReranker(base)
|
||||||
|
} else if (base.rerankModelProvider === 'jina') {
|
||||||
|
return new JinaReranker(base)
|
||||||
|
}
|
||||||
|
return new DefaultReranker(base)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { KnowledgeBaseParams } from '@types'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import BaseReranker from './BaseReranker'
|
||||||
|
|
||||||
|
export default class SiliconFlowReranker extends BaseReranker {
|
||||||
|
constructor(base: KnowledgeBaseParams) {
|
||||||
|
super(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
|
||||||
|
const baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||||
|
? this.base.rerankBaseURL.slice(0, -1)
|
||||||
|
: this.base.rerankBaseURL
|
||||||
|
const url = `${baseURL}/rerank`
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
model: this.base.rerankModel,
|
||||||
|
query,
|
||||||
|
documents: searchResults.map((doc) => doc.pageContent),
|
||||||
|
top_n: this.base.topN,
|
||||||
|
max_chunks_per_doc: this.base.chunkSize,
|
||||||
|
overlap_tokens: this.base.chunkOverlap
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() })
|
||||||
|
|
||||||
|
const rerankResults = data.results
|
||||||
|
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
|
||||||
|
|
||||||
|
return searchResults
|
||||||
|
.map((doc: ExtractChunkData, index: number) => {
|
||||||
|
const score = resultMap.get(index)
|
||||||
|
if (score === undefined) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
score
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SiliconFlow Reranker API 错误:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 353 KiB |
@@ -1,57 +1,35 @@
|
|||||||
import { BrowserWindow, dialog } from 'electron'
|
import { UpdateInfo } from 'builder-util-runtime'
|
||||||
|
import { app, BrowserWindow, dialog } from 'electron'
|
||||||
import logger from 'electron-log'
|
import logger from 'electron-log'
|
||||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
|
||||||
|
|
||||||
|
import icon from '../../../build/icon.png?asset'
|
||||||
|
|
||||||
export default class AppUpdater {
|
export default class AppUpdater {
|
||||||
autoUpdater: _AppUpdater = autoUpdater
|
autoUpdater: _AppUpdater = autoUpdater
|
||||||
|
private releaseInfo: UpdateInfo | undefined
|
||||||
|
|
||||||
constructor(mainWindow: BrowserWindow) {
|
constructor(mainWindow: BrowserWindow) {
|
||||||
logger.transports.file.level = 'debug'
|
logger.transports.file.level = 'info'
|
||||||
|
|
||||||
autoUpdater.logger = logger
|
autoUpdater.logger = logger
|
||||||
autoUpdater.forceDevUpdateConfig = true
|
autoUpdater.forceDevUpdateConfig = !app.isPackaged
|
||||||
autoUpdater.autoDownload = false
|
autoUpdater.autoDownload = true
|
||||||
|
|
||||||
// 检测下载错误
|
// 检测下载错误
|
||||||
autoUpdater.on('error', (error) => {
|
autoUpdater.on('error', (error) => {
|
||||||
logger.error('更新异常', error)
|
// 简单记录错误信息和时间戳
|
||||||
|
logger.error('更新异常', {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
time: new Date().toISOString()
|
||||||
|
})
|
||||||
mainWindow.webContents.send('update-error', error)
|
mainWindow.webContents.send('update-error', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
logger.info('检测到新版本', releaseInfo)
|
||||||
mainWindow.webContents.send('update-available', releaseInfo)
|
mainWindow.webContents.send('update-available', releaseInfo)
|
||||||
|
|
||||||
const releaseNotes = releaseInfo.releaseNotes
|
|
||||||
let releaseContent = ''
|
|
||||||
|
|
||||||
if (releaseNotes) {
|
|
||||||
if (typeof releaseNotes === 'string') {
|
|
||||||
releaseContent = <string>releaseNotes
|
|
||||||
} else if (releaseNotes instanceof Array) {
|
|
||||||
releaseNotes.forEach((releaseNote) => {
|
|
||||||
releaseContent += `${releaseNote}\n`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
releaseContent = '暂无更新说明'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 弹框确认是否下载更新(releaseContent是更新日志)
|
|
||||||
dialog
|
|
||||||
.showMessageBox({
|
|
||||||
type: 'info',
|
|
||||||
title: '应用有新的更新',
|
|
||||||
detail: releaseContent,
|
|
||||||
message: '发现新版本,是否现在更新?',
|
|
||||||
buttons: ['下次再说', '更新']
|
|
||||||
})
|
|
||||||
.then(({ response }) => {
|
|
||||||
if (response === 1) {
|
|
||||||
logger.info('用户选择更新,准备下载更新')
|
|
||||||
mainWindow.webContents.send('download-update')
|
|
||||||
autoUpdater.downloadUpdate()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检测到不需要更新时
|
// 检测到不需要更新时
|
||||||
@@ -61,23 +39,59 @@ export default class AppUpdater {
|
|||||||
|
|
||||||
// 更新下载进度
|
// 更新下载进度
|
||||||
autoUpdater.on('download-progress', (progress) => {
|
autoUpdater.on('download-progress', (progress) => {
|
||||||
logger.info('下载进度', progress)
|
|
||||||
mainWindow.webContents.send('download-progress', progress)
|
mainWindow.webContents.send('download-progress', progress)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 当需要更新的内容下载完成后
|
// 当需要更新的内容下载完成后
|
||||||
autoUpdater.on('update-downloaded', () => {
|
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
||||||
logger.info('下载完成,准备更新')
|
mainWindow.webContents.send('update-downloaded', releaseInfo)
|
||||||
dialog
|
this.releaseInfo = releaseInfo
|
||||||
.showMessageBox({
|
logger.info('下载完成', releaseInfo)
|
||||||
title: '安装更新',
|
|
||||||
message: '更新下载完毕,应用将重启并进行安装'
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
setImmediate(() => autoUpdater.quitAndInstall())
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.autoUpdater = autoUpdater
|
this.autoUpdater = autoUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async showUpdateDialog(mainWindow: BrowserWindow) {
|
||||||
|
if (!this.releaseInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog
|
||||||
|
.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: '安装更新',
|
||||||
|
icon,
|
||||||
|
message: `新版本 ${this.releaseInfo.version} 已准备就绪`,
|
||||||
|
detail: this.formatReleaseNotes(this.releaseInfo.releaseNotes),
|
||||||
|
buttons: ['稍后安装', '立即安装'],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 0
|
||||||
|
})
|
||||||
|
.then(({ response }) => {
|
||||||
|
if (response === 1) {
|
||||||
|
app.isQuitting = true
|
||||||
|
setImmediate(() => autoUpdater.quitAndInstall())
|
||||||
|
} else {
|
||||||
|
mainWindow.webContents.send('update-downloaded-cancelled')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
||||||
|
if (!releaseNotes) {
|
||||||
|
return '暂无更新说明'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof releaseNotes === 'string') {
|
||||||
|
return releaseNotes
|
||||||
|
}
|
||||||
|
|
||||||
|
return releaseNotes.map((note) => note.note).join('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReleaseNoteInfo {
|
||||||
|
readonly version: string
|
||||||
|
readonly note: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { WebDavConfig } from '@types'
|
import { WebDavConfig } from '@types'
|
||||||
import archiver from 'archiver'
|
import AdmZip from 'adm-zip'
|
||||||
|
import { exec } from 'child_process'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
import * as fs from 'fs-extra'
|
import * as fs from 'fs-extra'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as unzipper from 'unzipper'
|
import { createClient, FileStat } from 'webdav'
|
||||||
|
|
||||||
import WebDav from './WebDav'
|
import WebDav from './WebDav'
|
||||||
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
class BackupManager {
|
class BackupManager {
|
||||||
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
|
||||||
@@ -17,6 +19,54 @@ class BackupManager {
|
|||||||
this.restore = this.restore.bind(this)
|
this.restore = this.restore.bind(this)
|
||||||
this.backupToWebdav = this.backupToWebdav.bind(this)
|
this.backupToWebdav = this.backupToWebdav.bind(this)
|
||||||
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
||||||
|
this.listWebdavFiles = this.listWebdavFiles.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setWritableRecursive(dirPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const items = await fs.readdir(dirPath, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = path.join(dirPath, item.name)
|
||||||
|
|
||||||
|
// 先处理子目录
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
await this.setWritableRecursive(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一设置权限(Windows需要特殊处理)
|
||||||
|
await this.forceSetWritable(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保根目录权限
|
||||||
|
await this.forceSetWritable(dirPath)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`权限设置失败:${dirPath}`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增跨平台权限设置方法
|
||||||
|
private async forceSetWritable(targetPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Windows系统需要先取消只读属性
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
await fs.chmod(targetPath, 0o666) // Windows会忽略权限位但能移除只读
|
||||||
|
} else {
|
||||||
|
const stats = await fs.stat(targetPath)
|
||||||
|
const mode = stats.isDirectory() ? 0o777 : 0o666
|
||||||
|
await fs.chmod(targetPath, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 双重保险:使用文件属性命令(Windows专用)
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
await exec(`attrib -R "${targetPath}" /L /D`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||||
|
Logger.warn(`权限设置警告:${targetPath}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async backup(
|
async backup(
|
||||||
@@ -25,71 +75,120 @@ class BackupManager {
|
|||||||
data: string,
|
data: string,
|
||||||
destinationPath: string = this.backupDir
|
destinationPath: string = this.backupDir
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const mainWindow = windowService.getMainWindow()
|
||||||
|
|
||||||
|
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
|
||||||
|
mainWindow?.webContents.send('backup-progress', processData)
|
||||||
|
Logger.log('[BackupManager] backup progress', processData)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建临时目录
|
|
||||||
await fs.ensureDir(this.tempDir)
|
await fs.ensureDir(this.tempDir)
|
||||||
|
onProgress({ stage: 'preparing', progress: 0, total: 100 })
|
||||||
|
|
||||||
// 将 data 写入临时文件
|
// 将 data 写入临时文件
|
||||||
const tempDataPath = path.join(this.tempDir, 'data.json')
|
const tempDataPath = path.join(this.tempDir, 'data.json')
|
||||||
await fs.writeFile(tempDataPath, data)
|
await fs.writeFile(tempDataPath, data)
|
||||||
|
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
|
||||||
|
|
||||||
// 复制 Data 目录到临时目录
|
// 复制 Data 目录到临时目录
|
||||||
const sourcePath = path.join(app.getPath('userData'), 'Data')
|
const sourcePath = path.join(app.getPath('userData'), 'Data')
|
||||||
const tempDataDir = path.join(this.tempDir, 'Data')
|
const tempDataDir = path.join(this.tempDir, 'Data')
|
||||||
await fs.copy(sourcePath, tempDataDir)
|
|
||||||
|
|
||||||
// 创建 zip 文件
|
// 获取源目录总大小
|
||||||
const output = fs.createWriteStream(path.join(destinationPath, fileName))
|
const totalSize = await this.getDirSize(sourcePath)
|
||||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
let copiedSize = 0
|
||||||
|
|
||||||
archive.pipe(output)
|
// 使用流式复制
|
||||||
archive.directory(this.tempDir, false)
|
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
|
||||||
await archive.finalize()
|
copiedSize += size
|
||||||
|
const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60))
|
||||||
|
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.setWritableRecursive(tempDataDir)
|
||||||
|
onProgress({ stage: 'compressing', progress: 80, total: 100 })
|
||||||
|
|
||||||
|
// 使用 adm-zip 创建压缩文件
|
||||||
|
const zip = new AdmZip()
|
||||||
|
zip.addLocalFolder(this.tempDir)
|
||||||
|
const backupedFilePath = path.join(destinationPath, fileName)
|
||||||
|
zip.writeZip(backupedFilePath)
|
||||||
|
|
||||||
// 清理临时目录
|
// 清理临时目录
|
||||||
await fs.remove(this.tempDir)
|
await fs.remove(this.tempDir)
|
||||||
|
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
||||||
|
|
||||||
Logger.log('Backup completed successfully')
|
Logger.log('[BackupManager] Backup completed successfully')
|
||||||
|
|
||||||
const backupedFilePath = path.join(destinationPath, fileName)
|
|
||||||
|
|
||||||
return backupedFilePath
|
return backupedFilePath
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('Backup failed:', error)
|
Logger.error('[BackupManager] Backup failed:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
||||||
// 创建临时目录
|
const mainWindow = windowService.getMainWindow()
|
||||||
await fs.ensureDir(this.tempDir)
|
|
||||||
|
|
||||||
// 解压备份文件到临时目录
|
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
|
||||||
await fs
|
mainWindow?.webContents.send('restore-progress', processData)
|
||||||
.createReadStream(backupPath)
|
Logger.log('[BackupManager] restore progress', processData)
|
||||||
.pipe(unzipper.Extract({ path: this.tempDir }))
|
}
|
||||||
.promise()
|
|
||||||
|
|
||||||
// 读取 data.json
|
try {
|
||||||
const dataPath = path.join(this.tempDir, 'data.json')
|
// 创建临时目录
|
||||||
const data = await fs.readFile(dataPath, 'utf-8')
|
await fs.ensureDir(this.tempDir)
|
||||||
|
onProgress({ stage: 'preparing', progress: 0, total: 100 })
|
||||||
|
|
||||||
// 恢复 Data 目录
|
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
|
||||||
const sourcePath = path.join(this.tempDir, 'Data')
|
// 使用 adm-zip 解压
|
||||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
const zip = new AdmZip(backupPath)
|
||||||
await fs.remove(destPath)
|
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
|
||||||
await fs.copy(sourcePath, destPath)
|
onProgress({ stage: 'extracting', progress: 20, total: 100 })
|
||||||
|
|
||||||
// 清理临时目录
|
Logger.log('[backup] step 2: read data.json')
|
||||||
await fs.remove(this.tempDir)
|
// 读取 data.json
|
||||||
|
const dataPath = path.join(this.tempDir, 'data.json')
|
||||||
|
const data = await fs.readFile(dataPath, 'utf-8')
|
||||||
|
onProgress({ stage: 'reading_data', progress: 40, total: 100 })
|
||||||
|
|
||||||
Logger.log('Restore completed successfully')
|
Logger.log('[backup] step 3: restore Data directory')
|
||||||
|
// 恢复 Data 目录
|
||||||
|
const sourcePath = path.join(this.tempDir, 'Data')
|
||||||
|
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||||
|
|
||||||
return data
|
// 获取源目录总大小
|
||||||
|
const totalSize = await this.getDirSize(sourcePath)
|
||||||
|
let copiedSize = 0
|
||||||
|
|
||||||
|
await this.setWritableRecursive(destPath)
|
||||||
|
await fs.remove(destPath)
|
||||||
|
|
||||||
|
// 使用流式复制
|
||||||
|
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
|
||||||
|
copiedSize += size
|
||||||
|
const progress = Math.min(90, 40 + Math.floor((copiedSize / totalSize) * 50))
|
||||||
|
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||||
|
})
|
||||||
|
|
||||||
|
Logger.log('[backup] step 4: clean up temp directory')
|
||||||
|
// 清理临时目录
|
||||||
|
await this.setWritableRecursive(this.tempDir)
|
||||||
|
await fs.remove(this.tempDir)
|
||||||
|
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
||||||
|
|
||||||
|
Logger.log('[backup] step 5: Restore completed successfully')
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[backup] Restore failed:', error)
|
||||||
|
await fs.remove(this.tempDir).catch(() => {})
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
||||||
const filename = 'cherry-studio.backup.zip'
|
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
|
||||||
const backupedFilePath = await this.backup(_, filename, data)
|
const backupedFilePath = await this.backup(_, filename, data)
|
||||||
const webdavClient = new WebDav(webdavConfig)
|
const webdavClient = new WebDav(webdavConfig)
|
||||||
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
||||||
@@ -98,18 +197,86 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
||||||
const filename = 'cherry-studio.backup.zip'
|
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
|
||||||
const webdavClient = new WebDav(webdavConfig)
|
const webdavClient = new WebDav(webdavConfig)
|
||||||
const retrievedFile = await webdavClient.getFileContents(filename)
|
try {
|
||||||
const backupedFilePath = path.join(this.backupDir, filename)
|
const retrievedFile = await webdavClient.getFileContents(filename)
|
||||||
|
const backupedFilePath = path.join(this.backupDir, filename)
|
||||||
|
|
||||||
if (!fs.existsSync(this.backupDir)) {
|
if (!fs.existsSync(this.backupDir)) {
|
||||||
fs.mkdirSync(this.backupDir, { recursive: true })
|
fs.mkdirSync(this.backupDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync为同步写,无须await
|
||||||
|
fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
||||||
|
|
||||||
|
return await this.restore(_, backupedFilePath)
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('[backup] Failed to restore from WebDAV:', error)
|
||||||
|
throw new Error(error.message || 'Failed to restore backup file')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => {
|
||||||
|
try {
|
||||||
|
const client = createClient(config.webdavHost, {
|
||||||
|
username: config.webdavUser,
|
||||||
|
password: config.webdavPass
|
||||||
|
})
|
||||||
|
|
||||||
return await this.restore(_, backupedFilePath)
|
const response = await client.getDirectoryContents(config.webdavPath)
|
||||||
|
const files = Array.isArray(response) ? response : response.data
|
||||||
|
|
||||||
|
return files
|
||||||
|
.filter((file: FileStat) => file.type === 'file' && file.basename.endsWith('.zip'))
|
||||||
|
.map((file: FileStat) => ({
|
||||||
|
fileName: file.basename,
|
||||||
|
modifiedTime: file.lastmod,
|
||||||
|
size: file.size
|
||||||
|
}))
|
||||||
|
.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('Failed to list WebDAV files:', error)
|
||||||
|
throw new Error(error.message || 'Failed to list backup files')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDirSize(dirPath: string): Promise<number> {
|
||||||
|
let size = 0
|
||||||
|
const items = await fs.readdir(dirPath, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = path.join(dirPath, item.name)
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
size += await this.getDirSize(fullPath)
|
||||||
|
} else {
|
||||||
|
const stats = await fs.stat(fullPath)
|
||||||
|
size += stats.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyDirWithProgress(
|
||||||
|
source: string,
|
||||||
|
destination: string,
|
||||||
|
onProgress: (size: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
const items = await fs.readdir(source, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const sourcePath = path.join(source, item.name)
|
||||||
|
const destPath = path.join(destination, item.name)
|
||||||
|
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
await fs.ensureDir(destPath)
|
||||||
|
await this.copyDirWithProgress(sourcePath, destPath, onProgress)
|
||||||
|
} else {
|
||||||
|
const stats = await fs.stat(sourcePath)
|
||||||
|
await fs.copy(sourcePath, destPath)
|
||||||
|
onProgress(stats.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
interface CacheItem<T> {
|
||||||
|
data: T
|
||||||
|
timestamp: number
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CacheService {
|
||||||
|
private static cache: Map<string, CacheItem<any>> = new Map()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cache
|
||||||
|
* @param key Cache key
|
||||||
|
* @param data Cache data
|
||||||
|
* @param duration Cache duration (in milliseconds)
|
||||||
|
*/
|
||||||
|
static set<T>(key: string, data: T, duration: number): void {
|
||||||
|
this.cache.set(key, {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
duration
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache
|
||||||
|
* @param key Cache key
|
||||||
|
* @returns Returns data if cache exists and not expired, otherwise returns null
|
||||||
|
*/
|
||||||
|
static get<T>(key: string): T | null {
|
||||||
|
const item = this.cache.get(key)
|
||||||
|
if (!item) return null
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - item.timestamp > item.duration) {
|
||||||
|
this.remove(key)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove specific cache
|
||||||
|
* @param key Cache key
|
||||||
|
*/
|
||||||
|
static remove(key: string): void {
|
||||||
|
this.cache.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cache
|
||||||
|
*/
|
||||||
|
static clear(): void {
|
||||||
|
this.cache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if cache exists and is valid
|
||||||
|
* @param key Cache key
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
static has(key: string): boolean {
|
||||||
|
const item = this.cache.get(key)
|
||||||
|
if (!item) return false
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - item.timestamp > item.duration) {
|
||||||
|
this.remove(key)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { debounce, getResourcePath } from '@main/utils'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { screen } from 'electron'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
|
export default class ClipboardMonitor {
|
||||||
|
private platform: string
|
||||||
|
private lastText: string
|
||||||
|
private user32: any
|
||||||
|
private observer: any
|
||||||
|
public onTextSelected: (text: string) => void
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.platform = process.platform
|
||||||
|
this.lastText = ''
|
||||||
|
this.onTextSelected = debounce((text: string) => this.handleTextSelected(text), 550)
|
||||||
|
|
||||||
|
if (this.platform === 'win32') {
|
||||||
|
this.setupWindows()
|
||||||
|
} else if (this.platform === 'darwin') {
|
||||||
|
this.setupMacOS()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMacOS() {
|
||||||
|
// 使用 Swift 脚本来监听文本选择
|
||||||
|
const scriptPath = path.join(getResourcePath(), 'textMonitor.swift')
|
||||||
|
|
||||||
|
// 启动 Swift 进程来监听文本选择
|
||||||
|
const process = exec(`swift ${scriptPath}`)
|
||||||
|
|
||||||
|
process?.stdout?.on('data', (data: string) => {
|
||||||
|
console.log('[ClipboardMonitor] MacOS data:', data)
|
||||||
|
const text = data.toString().trim()
|
||||||
|
if (text && text !== this.lastText) {
|
||||||
|
this.lastText = text
|
||||||
|
this.onTextSelected(text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('error', (error) => {
|
||||||
|
console.error('[ClipboardMonitor] MacOS error:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setupWindows() {
|
||||||
|
// 使用 Windows API 监听文本选择事件
|
||||||
|
const ffi = require('ffi-napi')
|
||||||
|
const ref = require('ref-napi')
|
||||||
|
|
||||||
|
this.user32 = new ffi.Library('user32', {
|
||||||
|
SetWinEventHook: ['pointer', ['uint32', 'uint32', 'pointer', 'pointer', 'uint32', 'uint32', 'uint32']],
|
||||||
|
UnhookWinEvent: ['bool', ['pointer']]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件常量
|
||||||
|
const EVENT_OBJECT_SELECTION = 0x8006
|
||||||
|
const WINEVENT_OUTOFCONTEXT = 0x0000
|
||||||
|
const WINEVENT_SKIPOWNTHREAD = 0x0001
|
||||||
|
const WINEVENT_SKIPOWNPROCESS = 0x0002
|
||||||
|
|
||||||
|
// 创建回调函数
|
||||||
|
const callback = ffi.Callback('void', ['pointer', 'uint32', 'pointer', 'long', 'long', 'uint32', 'uint32'], () => {
|
||||||
|
this.getSelectedText()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置事件钩子
|
||||||
|
this.observer = this.user32.SetWinEventHook(
|
||||||
|
EVENT_OBJECT_SELECTION,
|
||||||
|
EVENT_OBJECT_SELECTION,
|
||||||
|
ref.NULL,
|
||||||
|
callback,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNTHREAD | WINEVENT_SKIPOWNPROCESS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedText() {
|
||||||
|
// Get selected text
|
||||||
|
if (this.platform === 'win32') {
|
||||||
|
const ref = require('ref-napi')
|
||||||
|
if (this.user32.OpenClipboard(ref.NULL)) {
|
||||||
|
// Get clipboard content
|
||||||
|
const text = this.user32.GetClipboardData(1) // CF_TEXT = 1
|
||||||
|
this.user32.CloseClipboard()
|
||||||
|
|
||||||
|
if (text && text !== this.lastText) {
|
||||||
|
this.lastText = text
|
||||||
|
this.onTextSelected(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTextSelected(text: string) {
|
||||||
|
if (!text) return
|
||||||
|
|
||||||
|
console.log('[ClipboardMonitor] handleTextSelected', text)
|
||||||
|
|
||||||
|
windowService.setLastSelectedText(text)
|
||||||
|
|
||||||
|
const mousePosition = screen.getCursorScreenPoint()
|
||||||
|
|
||||||
|
windowService.showSelectionMenu({
|
||||||
|
x: mousePosition.x,
|
||||||
|
y: mousePosition.y + 10
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this.platform === 'win32' && this.observer) {
|
||||||
|
this.user32.UnhookWinEvent(this.observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { ZOOM_SHORTCUTS } from '@shared/config/constant'
|
||||||
|
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
|
||||||
|
import { app } from 'electron'
|
||||||
|
import Store from 'electron-store'
|
||||||
|
|
||||||
|
import { locales } from '../utils/locales'
|
||||||
|
|
||||||
|
export class ConfigManager {
|
||||||
|
private store: Store
|
||||||
|
private subscribers: Map<string, Array<(newValue: any) => void>> = new Map()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.store = new Store()
|
||||||
|
}
|
||||||
|
|
||||||
|
getLanguage(): LanguageVarious {
|
||||||
|
const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : 'en-US'
|
||||||
|
return this.store.get('language', locale) as LanguageVarious
|
||||||
|
}
|
||||||
|
|
||||||
|
setLanguage(theme: LanguageVarious) {
|
||||||
|
this.store.set('language', theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTheme(): ThemeMode {
|
||||||
|
return this.store.get('theme', ThemeMode.light) as ThemeMode
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(theme: ThemeMode) {
|
||||||
|
this.store.set('theme', theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTray(): boolean {
|
||||||
|
return !!this.store.get('tray', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTray(value: boolean) {
|
||||||
|
this.store.set('tray', value)
|
||||||
|
this.notifySubscribers('tray', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
getZoomFactor(): number {
|
||||||
|
return this.store.get('zoomFactor', 1) as number
|
||||||
|
}
|
||||||
|
|
||||||
|
setZoomFactor(factor: number) {
|
||||||
|
this.store.set('zoomFactor', factor)
|
||||||
|
this.notifySubscribers('zoomFactor', factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe<T>(key: string, callback: (newValue: T) => void) {
|
||||||
|
if (!this.subscribers.has(key)) {
|
||||||
|
this.subscribers.set(key, [])
|
||||||
|
}
|
||||||
|
this.subscribers.get(key)!.push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe<T>(key: string, callback: (newValue: T) => void) {
|
||||||
|
const subscribers = this.subscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
this.subscribers.set(
|
||||||
|
key,
|
||||||
|
subscribers.filter((subscriber) => subscriber !== callback)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifySubscribers<T>(key: string, newValue: T) {
|
||||||
|
const subscribers = this.subscribers.get(key)
|
||||||
|
if (subscribers) {
|
||||||
|
subscribers.forEach((subscriber) => subscriber(newValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getShortcuts() {
|
||||||
|
return this.store.get('shortcuts', ZOOM_SHORTCUTS) as Shortcut[] | []
|
||||||
|
}
|
||||||
|
|
||||||
|
setShortcuts(shortcuts: Shortcut[]) {
|
||||||
|
this.store.set(
|
||||||
|
'shortcuts',
|
||||||
|
shortcuts.filter((shortcut) => shortcut.system)
|
||||||
|
)
|
||||||
|
this.notifySubscribers('shortcuts', shortcuts)
|
||||||
|
}
|
||||||
|
|
||||||
|
getClickTrayToShowQuickAssistant(): boolean {
|
||||||
|
return this.store.get('clickTrayToShowQuickAssistant', false) as boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
setClickTrayToShowQuickAssistant(value: boolean) {
|
||||||
|
this.store.set('clickTrayToShowQuickAssistant', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnableQuickAssistant(): boolean {
|
||||||
|
return this.store.get('enableQuickAssistant', false) as boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnableQuickAssistant(value: boolean) {
|
||||||
|
this.store.set('enableQuickAssistant', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: any) {
|
||||||
|
this.store.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string) {
|
||||||
|
return this.store.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configManager = new ConfigManager()
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import axios, { AxiosRequestConfig } from 'axios'
|
||||||
|
import { app, safeStorage } from 'electron'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// 配置常量,集中管理
|
||||||
|
const CONFIG = {
|
||||||
|
GITHUB_CLIENT_ID: 'Iv1.b507a08c87ecfe98',
|
||||||
|
POLLING: {
|
||||||
|
MAX_ATTEMPTS: 8,
|
||||||
|
INITIAL_DELAY_MS: 1000,
|
||||||
|
MAX_DELAY_MS: 16000 // 最大延迟16秒
|
||||||
|
},
|
||||||
|
DEFAULT_HEADERS: {
|
||||||
|
accept: 'application/json',
|
||||||
|
'editor-version': 'Neovim/0.6.1',
|
||||||
|
'editor-plugin-version': 'copilot.vim/1.16.0',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'user-agent': 'GithubCopilot/1.155.0',
|
||||||
|
'accept-encoding': 'gzip,deflate,br'
|
||||||
|
},
|
||||||
|
// API端点集中管理
|
||||||
|
API_URLS: {
|
||||||
|
GITHUB_USER: 'https://api.github.com/user',
|
||||||
|
GITHUB_DEVICE_CODE: 'https://github.com/login/device/code',
|
||||||
|
GITHUB_ACCESS_TOKEN: 'https://github.com/login/oauth/access_token',
|
||||||
|
COPILOT_TOKEN: 'https://api.github.com/copilot_internal/v2/token'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接口定义移到顶部,便于查阅
|
||||||
|
interface UserResponse {
|
||||||
|
login: string
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthResponse {
|
||||||
|
device_code: string
|
||||||
|
user_code: string
|
||||||
|
verification_uri: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenResponse {
|
||||||
|
access_token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotTokenResponse {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义错误类,统一错误处理
|
||||||
|
class CopilotServiceError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly cause?: unknown
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'CopilotServiceError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CopilotService {
|
||||||
|
private readonly tokenFilePath: string
|
||||||
|
private headers: Record<string, string>
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.tokenFilePath = path.join(app.getPath('userData'), '.copilot_token')
|
||||||
|
this.headers = { ...CONFIG.DEFAULT_HEADERS }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置自定义请求头
|
||||||
|
*/
|
||||||
|
private updateHeaders = (headers?: Record<string, string>): void => {
|
||||||
|
if (headers && Object.keys(headers).length > 0) {
|
||||||
|
this.headers = { ...headers }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取GitHub登录信息
|
||||||
|
*/
|
||||||
|
public getUser = async (_: Electron.IpcMainInvokeEvent, token: string): Promise<UserResponse> => {
|
||||||
|
try {
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
headers: {
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
'user-agent': 'Visual Studio Code (desktop)',
|
||||||
|
'Sec-Fetch-Site': 'none',
|
||||||
|
'Sec-Fetch-Mode': 'no-cors',
|
||||||
|
'Sec-Fetch-Dest': 'empty',
|
||||||
|
authorization: `token ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get(CONFIG.API_URLS.GITHUB_USER, config)
|
||||||
|
return {
|
||||||
|
login: response.data.login,
|
||||||
|
avatar: response.data.avatar_url
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get user information:', error)
|
||||||
|
throw new CopilotServiceError('无法获取GitHub用户信息', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取GitHub设备授权信息
|
||||||
|
*/
|
||||||
|
public getAuthMessage = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<AuthResponse> => {
|
||||||
|
try {
|
||||||
|
this.updateHeaders(headers)
|
||||||
|
|
||||||
|
const response = await axios.post<AuthResponse>(
|
||||||
|
CONFIG.API_URLS.GITHUB_DEVICE_CODE,
|
||||||
|
{
|
||||||
|
client_id: CONFIG.GITHUB_CLIENT_ID,
|
||||||
|
scope: 'read:user'
|
||||||
|
},
|
||||||
|
{ headers: this.headers }
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get auth message:', error)
|
||||||
|
throw new CopilotServiceError('无法获取GitHub授权信息', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用设备码获取访问令牌 - 优化轮询逻辑
|
||||||
|
*/
|
||||||
|
public getCopilotToken = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
device_code: string,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<TokenResponse> => {
|
||||||
|
this.updateHeaders(headers)
|
||||||
|
|
||||||
|
let currentDelay = CONFIG.POLLING.INITIAL_DELAY_MS
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < CONFIG.POLLING.MAX_ATTEMPTS; attempt++) {
|
||||||
|
await this.delay(currentDelay)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post<TokenResponse>(
|
||||||
|
CONFIG.API_URLS.GITHUB_ACCESS_TOKEN,
|
||||||
|
{
|
||||||
|
client_id: CONFIG.GITHUB_CLIENT_ID,
|
||||||
|
device_code,
|
||||||
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
||||||
|
},
|
||||||
|
{ headers: this.headers }
|
||||||
|
)
|
||||||
|
|
||||||
|
const { access_token } = response.data
|
||||||
|
if (access_token) {
|
||||||
|
return { access_token }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 指数退避策略
|
||||||
|
currentDelay = Math.min(currentDelay * 2, CONFIG.POLLING.MAX_DELAY_MS)
|
||||||
|
|
||||||
|
// 仅在最后一次尝试失败时记录详细错误
|
||||||
|
const isLastAttempt = attempt === CONFIG.POLLING.MAX_ATTEMPTS - 1
|
||||||
|
if (isLastAttempt) {
|
||||||
|
console.error(`Token polling failed after ${CONFIG.POLLING.MAX_ATTEMPTS} attempts:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CopilotServiceError('获取访问令牌超时,请重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存Copilot令牌到本地文件
|
||||||
|
*/
|
||||||
|
public saveCopilotToken = async (_: Electron.IpcMainInvokeEvent, token: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const encryptedToken = safeStorage.encryptString(token)
|
||||||
|
await fs.writeFile(this.tokenFilePath, encryptedToken)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save token:', error)
|
||||||
|
throw new CopilotServiceError('无法保存访问令牌', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地文件读取令牌并获取Copilot令牌
|
||||||
|
*/
|
||||||
|
public getToken = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<CopilotTokenResponse> => {
|
||||||
|
try {
|
||||||
|
this.updateHeaders(headers)
|
||||||
|
|
||||||
|
const encryptedToken = await fs.readFile(this.tokenFilePath)
|
||||||
|
const access_token = safeStorage.decryptString(Buffer.from(encryptedToken))
|
||||||
|
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
authorization: `token ${access_token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get<CopilotTokenResponse>(CONFIG.API_URLS.COPILOT_TOKEN, config)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get Copilot token:', error)
|
||||||
|
throw new CopilotServiceError('无法获取Copilot令牌,请重新授权', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录,删除本地token文件
|
||||||
|
*/
|
||||||
|
public logout = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await fs.access(this.tokenFilePath)
|
||||||
|
await fs.unlink(this.tokenFilePath)
|
||||||
|
console.log('Successfully logged out from Copilot')
|
||||||
|
} catch (error) {
|
||||||
|
// 文件不存在不是错误,只是记录一下
|
||||||
|
console.log('Token file not found, nothing to delete')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to logout:', error)
|
||||||
|
throw new CopilotServiceError('无法完成退出登录操作', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助方法:延迟执行
|
||||||
|
*/
|
||||||
|
private delay = (ms: number): Promise<void> => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CopilotService()
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
|
// ExportService
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlignmentType,
|
||||||
|
BorderStyle,
|
||||||
|
Document,
|
||||||
|
ExternalHyperlink,
|
||||||
|
HeadingLevel,
|
||||||
|
Packer,
|
||||||
|
Paragraph,
|
||||||
|
ShadingType,
|
||||||
|
Table,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
TextRun,
|
||||||
|
VerticalAlign,
|
||||||
|
WidthType
|
||||||
|
} from 'docx'
|
||||||
|
import { dialog } from 'electron'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
import FileStorage from './FileStorage'
|
||||||
|
|
||||||
|
export class ExportService {
|
||||||
|
private fileManager: FileStorage
|
||||||
|
private md: MarkdownIt
|
||||||
|
|
||||||
|
constructor(fileManager: FileStorage) {
|
||||||
|
this.fileManager = fileManager
|
||||||
|
this.md = new MarkdownIt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertMarkdownToDocxElements(markdown: string) {
|
||||||
|
const tokens = this.md.parse(markdown, {})
|
||||||
|
const elements: any[] = []
|
||||||
|
let listLevel = 0
|
||||||
|
let currentTable: Table | null = null
|
||||||
|
let currentRowCells: TableCell[] = []
|
||||||
|
let isHeaderRow = false
|
||||||
|
let tableColumnCount = 0
|
||||||
|
let tableRows: TableRow[] = [] // Store rows temporarily
|
||||||
|
|
||||||
|
const processInlineTokens = (tokens: any[], isHeaderRow: boolean): (TextRun | ExternalHyperlink)[] => {
|
||||||
|
const runs: (TextRun | ExternalHyperlink)[] = []
|
||||||
|
let linkText = ''
|
||||||
|
let linkUrl = ''
|
||||||
|
let insideLink = false
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i]
|
||||||
|
switch (token.type) {
|
||||||
|
case 'link_open':
|
||||||
|
insideLink = true
|
||||||
|
linkUrl = token.attrs.find((attr: [string, string]) => attr[0] === 'href')[1]
|
||||||
|
linkText = tokens[i + 1].content
|
||||||
|
i += 1
|
||||||
|
break
|
||||||
|
case 'link_close':
|
||||||
|
if (insideLink && linkUrl && linkText) {
|
||||||
|
// Handle any accumulated link text with the ExternalHyperlink
|
||||||
|
runs.push(
|
||||||
|
new ExternalHyperlink({
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: linkText,
|
||||||
|
style: 'Hyperlink',
|
||||||
|
color: '0000FF',
|
||||||
|
underline: {
|
||||||
|
type: 'single'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
link: linkUrl
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reset link variables
|
||||||
|
linkText = ''
|
||||||
|
linkUrl = ''
|
||||||
|
insideLink = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'text':
|
||||||
|
runs.push(new TextRun({ text: token.content, bold: isHeaderRow ? true : false }))
|
||||||
|
break
|
||||||
|
case 'strong':
|
||||||
|
runs.push(new TextRun({ text: token.content, bold: true }))
|
||||||
|
break
|
||||||
|
case 'em':
|
||||||
|
runs.push(new TextRun({ text: token.content, italics: true }))
|
||||||
|
break
|
||||||
|
case 'code_inline':
|
||||||
|
runs.push(new TextRun({ text: token.content, font: 'Consolas', size: 20 }))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runs
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i]
|
||||||
|
switch (token.type) {
|
||||||
|
case 'heading_open':
|
||||||
|
// 获取标题级别 (h1 -> h6)
|
||||||
|
const level = parseInt(token.tag.slice(1)) as 1 | 2 | 3 | 4 | 5 | 6
|
||||||
|
const headingText = tokens[i + 1].content
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
text: headingText,
|
||||||
|
heading: HeadingLevel[`HEADING_${level}`],
|
||||||
|
spacing: {
|
||||||
|
before: 240,
|
||||||
|
after: 120
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
i += 2 // 跳过内容标记和闭合标记
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'paragraph_open':
|
||||||
|
const inlineTokens = tokens[i + 1].children || []
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: processInlineTokens(inlineTokens, false),
|
||||||
|
spacing: {
|
||||||
|
before: 120,
|
||||||
|
after: 120
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
i += 2
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'bullet_list_open':
|
||||||
|
listLevel++
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'bullet_list_close':
|
||||||
|
listLevel--
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'list_item_open':
|
||||||
|
const itemInlineTokens = tokens[i + 2].children || []
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [
|
||||||
|
new TextRun({ text: '•', bold: true }),
|
||||||
|
new TextRun({ text: '\t' }),
|
||||||
|
...processInlineTokens(itemInlineTokens, false)
|
||||||
|
],
|
||||||
|
indent: {
|
||||||
|
left: listLevel * 720
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
i += 3
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'fence': // 代码块
|
||||||
|
const codeLines = token.content.split('\n')
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: codeLines.map(
|
||||||
|
(line) =>
|
||||||
|
new TextRun({
|
||||||
|
text: line + '\n',
|
||||||
|
font: 'Consolas',
|
||||||
|
size: 20,
|
||||||
|
break: 1
|
||||||
|
})
|
||||||
|
),
|
||||||
|
shading: {
|
||||||
|
type: ShadingType.SOLID,
|
||||||
|
color: 'F5F5F5'
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
before: 120,
|
||||||
|
after: 120
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
top: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
|
||||||
|
bottom: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
|
||||||
|
left: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
|
||||||
|
right: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'hr':
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [new TextRun({ text: '─'.repeat(50), color: '999999' })],
|
||||||
|
alignment: AlignmentType.CENTER
|
||||||
|
})
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'blockquote_open':
|
||||||
|
const quoteText = tokens[i + 2].content
|
||||||
|
elements.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [
|
||||||
|
new TextRun({
|
||||||
|
text: quoteText,
|
||||||
|
italics: true
|
||||||
|
})
|
||||||
|
],
|
||||||
|
indent: {
|
||||||
|
left: 720
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
left: {
|
||||||
|
style: BorderStyle.SINGLE,
|
||||||
|
size: 3,
|
||||||
|
color: 'CCCCCC'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
before: 120,
|
||||||
|
after: 120
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
i += 3
|
||||||
|
break
|
||||||
|
|
||||||
|
// 表格处理
|
||||||
|
case 'table_open':
|
||||||
|
tableRows = [] // Reset table rows for new table
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'thead_open':
|
||||||
|
isHeaderRow = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tbody_open':
|
||||||
|
isHeaderRow = false
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tr_open':
|
||||||
|
currentRowCells = []
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tr_close':
|
||||||
|
const row = new TableRow({
|
||||||
|
children: currentRowCells,
|
||||||
|
tableHeader: isHeaderRow
|
||||||
|
})
|
||||||
|
tableRows.push(row)
|
||||||
|
// 计算表格有多少列(针对第一行)
|
||||||
|
if (tableColumnCount === 0) {
|
||||||
|
tableColumnCount = currentRowCells.length
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'th_open':
|
||||||
|
case 'td_open':
|
||||||
|
const isFirstColumn = currentRowCells.length === 0 // 判断是否是第一列
|
||||||
|
const borders = {
|
||||||
|
top: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
bottom: isHeaderRow
|
||||||
|
? {
|
||||||
|
style: BorderStyle.SINGLE,
|
||||||
|
size: 0.5,
|
||||||
|
color: '000000'
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cellContent = tokens[i + 1]
|
||||||
|
const cellOptions = {
|
||||||
|
children: [
|
||||||
|
new Paragraph({
|
||||||
|
children: cellContent.children
|
||||||
|
? processInlineTokens(cellContent.children, isHeaderRow || isFirstColumn)
|
||||||
|
: [new TextRun({ text: cellContent.content || '', bold: isHeaderRow || isFirstColumn })],
|
||||||
|
alignment: AlignmentType.CENTER
|
||||||
|
})
|
||||||
|
],
|
||||||
|
verticalAlign: VerticalAlign.CENTER,
|
||||||
|
borders: borders
|
||||||
|
}
|
||||||
|
currentRowCells.push(new TableCell(cellOptions))
|
||||||
|
i += 2 // 跳过内容和结束标记
|
||||||
|
break
|
||||||
|
case 'table_close':
|
||||||
|
// Create table with the collected rows - avoid using protected properties
|
||||||
|
// Create the table with all rows
|
||||||
|
currentTable = new Table({
|
||||||
|
width: {
|
||||||
|
size: 100,
|
||||||
|
type: WidthType.PERCENTAGE
|
||||||
|
},
|
||||||
|
rows: tableRows,
|
||||||
|
borders: {
|
||||||
|
top: {
|
||||||
|
style: BorderStyle.SINGLE,
|
||||||
|
size: 1,
|
||||||
|
color: '000000'
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
style: BorderStyle.SINGLE,
|
||||||
|
size: 1,
|
||||||
|
color: '000000'
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
insideHorizontal: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
},
|
||||||
|
insideVertical: {
|
||||||
|
style: BorderStyle.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
elements.push(currentTable)
|
||||||
|
currentTable = null
|
||||||
|
tableColumnCount = 0
|
||||||
|
tableRows = []
|
||||||
|
currentRowCells = []
|
||||||
|
isHeaderRow = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportToWord = async (_: Electron.IpcMainInvokeEvent, markdown: string, fileName: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const elements = this.convertMarkdownToDocxElements(markdown)
|
||||||
|
|
||||||
|
const doc = new Document({
|
||||||
|
styles: {
|
||||||
|
paragraphStyles: [
|
||||||
|
{
|
||||||
|
id: 'Normal',
|
||||||
|
name: 'Normal',
|
||||||
|
run: {
|
||||||
|
size: 24,
|
||||||
|
font: 'Arial'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
properties: {},
|
||||||
|
children: elements
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const buffer = await Packer.toBuffer(doc)
|
||||||
|
|
||||||
|
const filePath = dialog.showSaveDialogSync({
|
||||||
|
title: '保存文件',
|
||||||
|
filters: [{ name: 'Word Document', extensions: ['docx'] }],
|
||||||
|
defaultPath: fileName
|
||||||
|
})
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
await this.fileManager.writeFile(_, filePath, buffer)
|
||||||
|
Logger.info('[ExportService] Document exported successfully')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[ExportService] Export to Word failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||