mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-08-22 15:27:13 +00:00
Compare commits
1272 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ccff4efbc | |||
| 37e024acea | |||
| b18b8ccb02 | |||
| f8a01cb9a2 | |||
| 64832a2a04 | |||
| b1dd781a3d | |||
| 71dc082e40 | |||
| 10836bbb4a | |||
| d8237ae301 | |||
| 3974f1be87 | |||
| 126c9921a8 | |||
| c84754fc0c | |||
| 172807d326 | |||
| 72608af77e | |||
| 7879459c3f | |||
| ad482dad7f | |||
| 58e99976b3 | |||
| 567803ebeb | |||
| 8e3d67cc7f | |||
| b463608ab7 | |||
| eefd36562d | |||
| ebf4497b34 | |||
| 194b0343bf | |||
| adb1dfbded | |||
| 9e689f6022 | |||
| 499f5c915f | |||
| a9f11d79a9 | |||
| db5e9ea2d0 | |||
| 64f8693871 | |||
| 4270847b5d | |||
| 3eb0177188 | |||
| 48203e0d31 | |||
| 6e6a496f1b | |||
| ddfa588163 | |||
| 809d8d71bd | |||
| 49254b2a32 | |||
| 5979451ea6 | |||
| 46c3d65bea | |||
| c4b44d84e3 | |||
| 822d1831cd | |||
| 0d28402aa3 | |||
| 359999c045 | |||
| 2c6eee7fc1 | |||
| c5badb3e13 | |||
| 65fb58585c | |||
| 02aedd9edc | |||
| 896b5de0a4 | |||
| 1ca58606da | |||
| 3d8c37cb38 | |||
| c8eec6c97e | |||
| 330ac397d4 | |||
| 8aa75a49d2 | |||
| 5d1cbb9050 | |||
| 8cf40ec6fd | |||
| 37b757d910 | |||
| ef6b642011 | |||
| 8b2cf90aab | |||
| 18961d3c26 | |||
| 976b71e592 | |||
| da63446e64 | |||
| 2522be1738 | |||
| 8f37ab859b | |||
| 1cb3c42bb9 | |||
| a60ffca135 | |||
| 1ff0636745 | |||
| 26f3db09a7 | |||
| 52d297624d | |||
| 53866d1461 | |||
| 54c8856adf | |||
| ead30c8779 | |||
| f8e4d2880f | |||
| 85b4cc0a3c | |||
| 3271b924fa | |||
| 2e2b4ee370 | |||
| 14524f0559 | |||
| 957954f5ee | |||
| 3531c7f356 | |||
| 9a4239290b | |||
| 0585edd895 | |||
| 52313fc7f6 | |||
| 9254b8fafe | |||
| c83c88ef27 | |||
| 3c065b99fb | |||
| 3a6f8ccc16 | |||
| 539e91c12e | |||
| 696ef20a80 | |||
| ebaaefaf7a | |||
| 77a8969f73 | |||
| b3b1981264 | |||
| b6d81890cf | |||
| 728de61bd6 | |||
| fb76e24c51 | |||
| d9a177864d | |||
| 536b4b8056 | |||
| 7fb0aad3c7 | |||
| b05334bb77 | |||
| b155597e28 | |||
| 0956bef9db | |||
| 6242f648f1 | |||
| c0832a24b9 | |||
| c9a7449d2d | |||
| f82270d097 | |||
| 5a4778074e | |||
| 9070531531 | |||
| ad52613738 | |||
| e7db720986 | |||
| c651103eff | |||
| 80ee41aee7 | |||
| 000969d902 | |||
| 9a94d98725 | |||
| c0a89d6f32 | |||
| 604ce985bd | |||
| 8c03ecad2b | |||
| af31f91973 | |||
| 0ce5df6811 | |||
| 4e237c9560 | |||
| e456210944 | |||
| cb7235bb83 | |||
| eab8265b9c | |||
| 5f24df6cee | |||
| e8e3783af0 | |||
| ec00f156f0 | |||
| bb6e90d50a | |||
| 8d2519d5a1 | |||
| 6c35c69ed7 | |||
| e994060e93 | |||
| f7cf992598 | |||
| 92915f7678 | |||
| 54f8494b5c | |||
| 068b5ddeef | |||
| 8168377e47 | |||
| 4d048dbfa7 | |||
| 0ee230cf41 | |||
| 9fba68fb14 | |||
| ff96fada02 | |||
| c19b7db8c0 | |||
| 6cfc7175e8 | |||
| a3f6a641aa | |||
| 54fe49de5d | |||
| 454dfc1aa7 | |||
| 3156701d4e | |||
| 41eb0e634a | |||
| 73d003d6c3 | |||
| 5e4ba6d971 | |||
| 76d32c78d8 | |||
| 66776556d8 | |||
| 149f598f6d | |||
| 1d6f0ab714 | |||
| 545f257476 | |||
| 6cb1f16f56 | |||
| 31fbf7e48a | |||
| e2e24078c5 | |||
| 39ef92e1ce | |||
| f63ba187dd | |||
| 23d1ac1021 | |||
| f08329eef4 | |||
| e993a582cd | |||
| 1524e189dc | |||
| 2b5165324c | |||
| 8f67826072 | |||
| ac9a31f049 | |||
| ed2cdbbc31 | |||
| 8bd2feaf7e | |||
| 4eccfe6d2c | |||
| 5ba2e000a1 | |||
| fe9adadb3a | |||
| 805c2a045e | |||
| fd7b196134 | |||
| 1b1c327c35 | |||
| a228f603d2 | |||
| 7d0a05ee11 | |||
| 5f0a62b63e | |||
| 825a1b1027 | |||
| 38dde1a373 | |||
| 950e7d1b00 | |||
| 89ded74345 | |||
| d8f9f48278 | |||
| 818bb38617 | |||
| bdd76addf3 | |||
| 0202d75ff4 | |||
| 2936f21f12 | |||
| a0831ec6cd | |||
| 705ca4d20a | |||
| 2926aab7a4 | |||
| 6a066f1b8e | |||
| 775fb1f853 | |||
| b7d137247a | |||
| 22278c40bf | |||
| 4373642ebd | |||
| e29b32bd2d | |||
| 5bf90920f5 | |||
| fa6bb5e46a | |||
| edcbb3e226 | |||
| cebf8497a4 | |||
| 5213bdf08b | |||
| cded4bdcc7 | |||
| cf817fd8ea | |||
| 596b700d63 | |||
| a2481ff1cf | |||
| 59dfa95bf8 | |||
| bc7d06d3e5 | |||
| f6b5a94b29 | |||
| 8e81dfa12a | |||
| 188fb23f08 | |||
| 0ff76f0f21 | |||
| 5347a12035 | |||
| 787caa84c8 | |||
| 691d453f41 | |||
| c2503e663a | |||
| 5b7f2603ae | |||
| 405a88862b | |||
| 3fd7f810f0 | |||
| 296eabe09a | |||
| e04a48623d | |||
| 2e1bad387c | |||
| 54b45ec2ff | |||
| 5c18a50330 | |||
| c434f85045 | |||
| d78d3ffe02 | |||
| 4d10279870 | |||
| 44240f65d5 | |||
| 97e81a7dcc | |||
| 9e8f1ed6bf | |||
| 4dbfdab50d | |||
| c39814ce2b | |||
| 95a071014c | |||
| 6e03f4b363 | |||
| 9de9489673 | |||
| bb8644dea0 | |||
| ff9142ddd4 | |||
| 9814fec930 | |||
| 35f469fb82 | |||
| 53ba731159 | |||
| 705ad58a8f | |||
| c96f86fbbf | |||
| 4b3b64e9e2 | |||
| 2f2e146951 | |||
| a09c529414 | |||
| a8d1d58e95 | |||
| 6695be815e | |||
| fbd3478772 | |||
| 2102e1afbb | |||
| 155c56f502 | |||
| 37a9b0e485 | |||
| 52a3f13f1e | |||
| a678a11c33 | |||
| d34b785238 | |||
| 1086e1d631 | |||
| b2f57aa483 | |||
| 3094b9c8fd | |||
| 4c2dba1004 | |||
| 0f0a4c0a7e | |||
| c374126f69 | |||
| 8498cd71dc | |||
| bf30517393 | |||
| 495f86e7fc | |||
| 471017657f | |||
| 4861dd75be | |||
| 342ceea371 | |||
| 880c34dfee | |||
| a1e487100d | |||
| 77948b1e16 | |||
| e28a12a1ee | |||
| 00a8bc6784 | |||
| 8fffa60569 | |||
| 2debe7e927 | |||
| 478bc32ddd | |||
| dfd2be1265 | |||
| 33ff2e29e5 | |||
| 4df1c7a136 | |||
| c522248d39 | |||
| 246d96ee63 | |||
| 909ae4aa00 | |||
| 26e3ababcf | |||
| 4d9f89f630 | |||
| b989199edb | |||
| 0d81fe6c8e | |||
| be45f41e34 | |||
| e9ac58b1ef | |||
| 59d9ae96ac | |||
| 3cff6f7189 | |||
| 14aee28289 | |||
| 8c1b4d4516 | |||
| 068df8fa15 | |||
| e371310d02 | |||
| 96b8121210 | |||
| 1960a85ead | |||
| 69cb479e01 | |||
| db90131bd6 | |||
| c18d272413 | |||
| cd0952e170 | |||
| e6a6d0485d | |||
| a642ae0332 | |||
| 67f508c7da | |||
| 2225f09797 | |||
| fe7fa46a0c | |||
| 6d8901fa21 | |||
| 7750cb0021 | |||
| 327d68a347 | |||
| 7c272b0efd | |||
| 5eb4ed157e | |||
| d4e00b960c | |||
| 79adc871ef | |||
| af0eff88b8 | |||
| c755befa28 | |||
| 3f6890da27 | |||
| 8144fada25 | |||
| 08319391ea | |||
| ae7f1c03e2 | |||
| 151017ed47 | |||
| 941d15b635 | |||
| d8058400dd | |||
| 5a1ed953de | |||
| 58d127b602 | |||
| 13bf73e6cf | |||
| aa46fdb113 | |||
| 4a09da3a25 | |||
| d70fcbb9f3 | |||
| a16ef6476d | |||
| dae75343d0 | |||
| d7564127d2 | |||
| c6bf54fd9d | |||
| 8d47072e1c | |||
| d601226187 | |||
| 4801832d9d | |||
| 6b0f42d0b8 | |||
| 08379c21ac | |||
| 2499bd9ad3 | |||
| ad6da0d320 | |||
| a2583c0591 | |||
| 0ea4a29152 | |||
| 7fe50788a5 | |||
| c4a68076d6 | |||
| 9e365895af | |||
| 9ece25e17d | |||
| 2140eff51b | |||
| dfba3c30a5 | |||
| 4b717109d2 | |||
| e54e908fbc | |||
| 7a11a9ef15 | |||
| c9d0700fd9 | |||
| f9b809801d | |||
| cc551ba266 | |||
| 754ba02263 | |||
| 23787ff462 | |||
| 7ddf57ae06 | |||
| 9f603c5e77 | |||
| 3d720bdd81 | |||
| cc5180a6f7 | |||
| a8eccbe43b | |||
| 2ac77ac39f | |||
| 4eaa518cf3 | |||
| 5622f94fc8 | |||
| 95457b7dcd | |||
| 6a9de72c78 | |||
| 088a614160 | |||
| 013ee98f53 | |||
| dc46ed0e05 | |||
| 2a0c657ca3 | |||
| a0aee80c63 | |||
| 4910be3403 | |||
| 1541d74c84 | |||
| 34c9151dc1 | |||
| 6c69770ed6 | |||
| 5b7c38c67f | |||
| b7bec8ecb7 | |||
| 82fe9c3596 | |||
| 9f44c34d34 | |||
| afd84516b0 | |||
| 024f00b73c | |||
| 24a21bf2ee | |||
| f22c6bf658 | |||
| 0df700ec18 | |||
| 46141f87b8 | |||
| eecce10018 | |||
| b793b81768 | |||
| c8506e3d3b | |||
| bddd611cc1 | |||
| b399fc557a | |||
| 233f6e00f0 | |||
| b20ea734fd | |||
| b7dba68549 | |||
| 7ab0acd6a4 | |||
| 1d60ccc516 | |||
| 2a8d2213b1 | |||
| a27ce36a32 | |||
| 3fdcc895ed | |||
| 88e510d07a | |||
| 2526feb0d9 | |||
| 4171b2bdfb | |||
| 0611f8de6c | |||
| df9e495dc5 | |||
| 8a48476105 | |||
| 9d4a1705a5 | |||
| 871e5733c7 | |||
| bdea12c51a | |||
| e8a663e3c7 | |||
| a27d9ea259 | |||
| 422d439627 | |||
| 7cd824c284 | |||
| edeff3c169 | |||
| e27d95e2b5 | |||
| 13076421a1 | |||
| 3943e7f05f | |||
| 6839827db0 | |||
| cd0d9dad98 | |||
| 857da34b9c | |||
| ccad7e7bb5 | |||
| b8de15d66e | |||
| c02661ea29 | |||
| 3c70c8ae59 | |||
| d6a04f96fe | |||
| b985b62068 | |||
| 297b760293 | |||
| 5f820b9dc1 | |||
| 088abfe7ab | |||
| 17340ae0ac | |||
| fe687af8ca | |||
| 7e26029268 | |||
| 03f7f2a53e | |||
| 296260bf6a | |||
| dd07acd21a | |||
| 89f6402fbf | |||
| 6788edbe9d | |||
| 853059c814 | |||
| 3895305882 | |||
| 5a0876f8dc | |||
| 2e32238652 | |||
| c2acbaaa94 | |||
| ad1a99aa44 | |||
| 24e4be019a | |||
| 13c917ad7e | |||
| 0a8cf6870f | |||
| 061291cebb | |||
| b8e0d7760b | |||
| 962de0183c | |||
| 627396dbf7 | |||
| 6c300bb018 | |||
| 9273df4af2 | |||
| 4a99be2f15 | |||
| 669d784f54 | |||
| 5253d657b6 | |||
| 27c816cf3b | |||
| c2548c5007 | |||
| 0d81776212 | |||
| bdec823148 | |||
| a6de958daa | |||
| cccab31c0f | |||
| 50c25d4574 | |||
| 68100f7f24 | |||
| 38777ea285 | |||
| 00c8f08179 | |||
| 45d6579fb6 | |||
| dad9254128 | |||
| 4ddf3bf2bf | |||
| 9934143f00 | |||
| f617bde81b | |||
| b2771b7b3f | |||
| 44b3237dd6 | |||
| b987a2d365 | |||
| 1cf299f090 | |||
| c87d78c666 | |||
| 3d37a3d367 | |||
| 79c8d90049 | |||
| 73d8236697 | |||
| 1afc3e5e1c | |||
| 87e8bbfa41 | |||
| 3e3d3e162e | |||
| cc80b87fee | |||
| b614ecccc2 | |||
| 8251c6589b | |||
| 4edf2ea02a | |||
| 12dd4659cd | |||
| 5d4f40c004 | |||
| 3c34e8e0e7 | |||
| 7441a3a717 | |||
| 3eef9a836b | |||
| 922202734a | |||
| fffd7e375f | |||
| d87f09c957 | |||
| b29884f8df | |||
| b270960a04 | |||
| c0ee2c2d8e | |||
| 5c4899df6e | |||
| be1580e949 | |||
| 6424eb871c | |||
| 8a8c43c7a5 | |||
| 9a503ddc72 | |||
| b130466c8f | |||
| 2f0215ac87 | |||
| 3ab5459778 | |||
| dd5cc206e5 | |||
| 4eb1bf2de2 | |||
| 6c47551b03 | |||
| 657ecccee3 | |||
| b4ba70c0f3 | |||
| c2bf5e845a | |||
| 8900e72e45 | |||
| 0cb192135d | |||
| c1ca687630 | |||
| 29cc24508b | |||
| 3db55cbd48 | |||
| c0a7f41747 | |||
| 1ba08bbfa6 | |||
| 51a8f42d89 | |||
| 3b081ff0f4 | |||
| 7f31a301e3 | |||
| 039d949523 | |||
| 5b16dc6ee7 | |||
| 34648c704f | |||
| 1966e69460 | |||
| 74b1b07b31 | |||
| 2e023cb8dc | |||
| 118c0e1ff1 | |||
| e933f32d9c | |||
| 81d075c028 | |||
| 514dd6c76a | |||
| 0b2501c1d8 | |||
| ddc323a8c3 | |||
| 2933c057a2 | |||
| c1d892069e | |||
| 20ed6d1d3d | |||
| 90f5275201 | |||
| fe64b203da | |||
| 9eb1353455 | |||
| 8a1c55c731 | |||
| 9059bebd07 | |||
| d18f79fe74 | |||
| 3a8f0be28a | |||
| 60cf380f96 | |||
| ab8240613e | |||
| d6e9dc6839 | |||
| 6891bdde53 | |||
| bda335212d | |||
| ecadfeab19 | |||
| 06f4cdc649 | |||
| a38d547200 | |||
| a15f431a7f | |||
| 3bb5b66e8b | |||
| 9bf7fa4081 | |||
| d9ff3242c6 | |||
| cd562ab8ed | |||
| 3a8a69ac2e | |||
| 4ca9dfd9c0 | |||
| 0d5b4936bd | |||
| b5f6eaf159 | |||
| adfee8bf58 | |||
| 25c80921e0 | |||
| fbfa2a71a9 | |||
| 5c51ed48c3 | |||
| 22febfc42a | |||
| de482dd9f4 | |||
| 74af67a41f | |||
| 170b41441b | |||
| 4991e50e16 | |||
| 1f236ef929 | |||
| 7f86c5984d | |||
| 6fcee49b19 | |||
| ac527851a0 | |||
| 64bbeaedcb | |||
| 3e984a1bcb | |||
| a7237fe62f | |||
| 79165214d7 | |||
| c3c454b7d7 | |||
| 3f7855dd6e | |||
| d4d708d44b | |||
| f0610281e5 | |||
| 7f0b6a3a46 | |||
| e87e7e2164 | |||
| c2a7c089d2 | |||
| 1a3ad390fd | |||
| df5bd4df60 | |||
| c383367e9f | |||
| 79b6010104 | |||
| 7a9f45c96b | |||
| 97b0a98793 | |||
| 7277cb289f | |||
| 86b2d5eff2 | |||
| 7873847616 | |||
| 5e46f149d9 | |||
| 574fc52332 | |||
| 8c924ca98f | |||
| a9adc0c1a2 | |||
| ed286c8894 | |||
| d900a3d08e | |||
| a55da15202 | |||
| cdf5b66729 | |||
| 9728118f94 | |||
| 9cc2ea412b | |||
| 3f1ad4b7dc | |||
| 56c225bf20 | |||
| 8b68bfb28c | |||
| a5299b52d4 | |||
| 9e9bc52737 | |||
| 76106e4504 | |||
| a6c00c42fa | |||
| 4e23005b5b | |||
| f27e2de220 | |||
| eefcabcff3 | |||
| 63e4669e3f | |||
| 7d5428f775 | |||
| ab3c4562fa | |||
| 13f89bf335 | |||
| e6a18445c3 | |||
| c8ab209426 | |||
| 360fea4085 | |||
| 9794d67eaa | |||
| b60a639312 | |||
| 870706c4ff | |||
| 491471fa10 | |||
| 6dbf61d4e4 | |||
| 81545d192b | |||
| 37cdc26160 | |||
| fb6a35ebe4 | |||
| f7a565bb80 | |||
| bf5e72b7e0 | |||
| ec6186596d | |||
| dece19cec6 | |||
| 6aa1d27711 | |||
| 264e77f383 | |||
| 0cbc284e42 | |||
| 9e42a334fa | |||
| 49438d2ee1 | |||
| 735aa24020 | |||
| 14f63a203d | |||
| e11c0a3633 | |||
| ca83f139f7 | |||
| 58e44b7d12 | |||
| 8f47474edd | |||
| c5200aada8 | |||
| 4e39b93673 | |||
| f215643f2e | |||
| aa72be6ff3 | |||
| b567e56b60 | |||
| 17431c1707 | |||
| 7c9ebef5e9 | |||
| 72b0e11543 | |||
| 9e2af9dbca | |||
| b7cc987132 | |||
| 8476c73b0c | |||
| e76b49afd2 | |||
| c712b95e0d | |||
| 1ee1a8d6d9 | |||
| f01c764589 | |||
| 6df66d150b | |||
| 6132f94eff | |||
| ac5e67ea73 | |||
| 143d2b44d0 | |||
| ef130fe377 | |||
| d7d1e2100c | |||
| af5afd7700 | |||
| a64df5cf0c | |||
| 3866319373 | |||
| ec5ad809f2 | |||
| 268acb174d | |||
| 3733877be1 | |||
| 6d5579a8d6 | |||
| 306cd2f945 | |||
| 5d4dd1e66f | |||
| 5f371bdc4a | |||
| aa58ba392e | |||
| 0485610818 | |||
| 7cb8becc09 | |||
| 8fa535a01b | |||
| 5f202e03a2 | |||
| ef2dfe330b | |||
| fcd86fbebd | |||
| 960d294aa2 | |||
| 27f8e63f3d | |||
| e8d7ad58be | |||
| f4d537e0f5 | |||
| d6c93dd537 | |||
| aa5fff47de | |||
| c920d8423d | |||
| b8fcfe2e7c | |||
| 072c96e1ec | |||
| 38c8ece642 | |||
| 9bf1576fb4 | |||
| d648e177c4 | |||
| e8f62af7f6 | |||
| e746aafa2f | |||
| 71f7a1b166 | |||
| 678f570a90 | |||
| 4bc1f195db | |||
| b2ff49ee94 | |||
| d9558f13ad | |||
| 8dc8f2567e | |||
| 316636f83c | |||
| e3c2fbf82a | |||
| 1b6175e598 | |||
| 26dc479596 | |||
| dca25dbb78 | |||
| c55275872e | |||
| d4d1fb26f6 | |||
| a48cb7bc34 | |||
| 8af6e9290f | |||
| 14b277d813 | |||
| 20915038a3 | |||
| 15723457cb | |||
| be8a0ec184 | |||
| ec79fc933c | |||
| b02e3aad95 | |||
| 3ed5a420a7 | |||
| 08eca511ad | |||
| 711e701933 | |||
| c34e911596 | |||
| e6bbd83c6b | |||
| 8a452c3072 | |||
| b99b310306 | |||
| 13bfb14107 | |||
| e3c46016e0 | |||
| 4188b0969e | |||
| eb739fcccb | |||
| 0c27795a10 | |||
| 9a94505f43 | |||
| d05693c5c1 | |||
| 7c5c3d8a3c | |||
| c0b2063b38 | |||
| 7027f9a55b | |||
| 4d183747b1 | |||
| cf3d8af260 | |||
| 08fe1b2f75 | |||
| 4ea7352674 | |||
| db3e8a267e | |||
| 6e6885849b | |||
| 8fc62682c4 | |||
| 827f2b9739 | |||
| 75031914a3 | |||
| 54d70623ab | |||
| a4c9fdd95a | |||
| ed819e8c79 | |||
| 6a9bfeb5aa | |||
| 94680c4411 | |||
| e654766f60 | |||
| 8bcbb68438 | |||
| 0ef6955f96 | |||
| b9f31d42f4 | |||
| b4501557c9 | |||
| dc2b768c6b | |||
| a2ed99e6cb | |||
| 60df732db7 | |||
| 6bd6bb3885 | |||
| 6d85d128cc | |||
| 399cf65fc9 | |||
| 96785ca037 | |||
| 24906a6df1 | |||
| 7cfc747653 | |||
| d772bbebe6 | |||
| 05f911628a | |||
| 14988853a3 | |||
| 18b718a328 | |||
| 7b3f16ac9f | |||
| 6d9b9bd3f7 | |||
| 82b2755c18 | |||
| 04e990aeb3 | |||
| cbbcd4c571 | |||
| 5ad9adeb4a | |||
| 13c9594be0 | |||
| e4f5f78bfa | |||
| bfa0208692 | |||
| 64994cdeda | |||
| 9796a841d9 | |||
| 404b493d23 | |||
| c04f9eb993 | |||
| 825da42208 | |||
| 721a8f1ecb | |||
| 586a174d6c | |||
| bec6966542 | |||
| b1f4e1d4d8 | |||
| d7d64e8259 | |||
| 08b498fccd | |||
| ecc5a52232 | |||
| 7327b90255 | |||
| 6ce7a9c16f | |||
| eb2658a91c | |||
| 9f59a18756 | |||
| ef06f0da98 | |||
| 4dec0e2c69 | |||
| 5f36f1af11 | |||
| 5a1a596098 | |||
| 7a25c6e062 | |||
| a73ed71b9d | |||
| 2abbb95ea2 | |||
| 1f80ba9463 | |||
| 4327fdac12 | |||
| a6f05a5874 | |||
| 3fb74a1598 | |||
| 2ac44cdeb6 | |||
| 4e4dc4cb73 | |||
| 855a953010 | |||
| 3264fda06e | |||
| 2c7d472069 | |||
| 8182e6797f | |||
| 99cb48ce88 | |||
| e391bfca80 | |||
| 0181ad3715 | |||
| b95dff0751 | |||
| 668d4c9c64 | |||
| 66c0d1b2f7 | |||
| bef0996375 | |||
| 391a1e4cf0 | |||
| 38facf517a | |||
| 5c9b1e8764 | |||
| dbcf14b566 | |||
| cc59f0c761 | |||
| f263fc067a | |||
| 6635b88baa | |||
| 87ea2a611f | |||
| ea8a2135cd | |||
| 76d86395ef | |||
| e268870636 | |||
| 49e9f41ef2 | |||
| 68cda968a1 | |||
| f6826fcefc | |||
| 56a91db7e7 | |||
| 8f4d94f046 | |||
| 9446bf5f98 | |||
| 23e353b18b | |||
| 46daaa4ba1 | |||
| f66d59531d | |||
| b76cd1d5ae | |||
| 55ba0d08a5 | |||
| cc78780a8e | |||
| 4a00809061 | |||
| cd760bbd84 | |||
| 84bdc6be7f | |||
| 8dd6bf8933 | |||
| 08f170d217 | |||
| 20da6a1c20 | |||
| 8586931630 | |||
| 667fc79a6f | |||
| ac7430dded | |||
| 9c73e16560 | |||
| c605e222a7 | |||
| 578f26a738 | |||
| 24218e7570 | |||
| 16f946550e | |||
| 9960a31485 | |||
| 437632f859 | |||
| 98f0f442eb | |||
| 3b999dc5e2 | |||
| 20943ad3d3 | |||
| 3b1544b5e4 | |||
| b317d597ac | |||
| 3bb19811e7 | |||
| 065a01de48 | |||
| 09fb2f6557 | |||
| 9ca97bf4bc | |||
| 0c59503edb | |||
| 6fa08861f8 | |||
| 72b95151a1 | |||
| 378e6ec9af | |||
| 27b68c1174 | |||
| 1ae8eabe99 | |||
| 72726c1cf9 | |||
| 6203d9a150 | |||
| 861f11af66 | |||
| 39fdfae541 | |||
| 9f57fb1421 | |||
| 059f57db2d | |||
| 13c8be8d06 | |||
| e0dbfb0488 | |||
| 722ed128a1 | |||
| cc628821e6 | |||
| 857af61af1 | |||
| f6fc484cb3 | |||
| ba07946f1e | |||
| 600f668c63 | |||
| 8a8990b12b | |||
| fe85c1bbb9 | |||
| a89ff72308 | |||
| a99e58184b | |||
| d981bc8fd0 | |||
| ff22480d89 | |||
| 8ec85e2829 | |||
| 706aacd8e6 | |||
| 803bc2d9fc | |||
| 7cc67cf8f0 | |||
| 9909c3a0ed | |||
| 60a3751839 | |||
| ce87e0c40b | |||
| 71cd548c00 | |||
| 1ddd05302b | |||
| ea3301c75c | |||
| 4b1c4f7ccc | |||
| 21f2622a4b | |||
| 79f6a43019 | |||
| 3d75093b2c | |||
| 023dc89c3e | |||
| b5641be30d | |||
| ff4515bbf1 | |||
| 5dc0fe05af | |||
| 25bba912f6 | |||
| cb6e323596 | |||
| cf4b04e047 | |||
| 243b5be31c | |||
| c3d753cf38 | |||
| 70c46d098b | |||
| 816e4a096e | |||
| 1d1e819817 | |||
| 342daf1be8 | |||
| f6bb9b582a | |||
| aad62bc976 | |||
| 38a49a7f37 | |||
| f603bf6be7 | |||
| 6cc05e9e27 | |||
| c00e67542f | |||
| 22660362a3 | |||
| b1ab9975b7 | |||
| 7ca89d8b54 | |||
| 998b07ab8c | |||
| 4936896ff7 | |||
| 18b7484c5b | |||
| 6754c8e85e | |||
| 57243ff010 | |||
| 7c4dfe96ee | |||
| bf19120c27 | |||
| 3d37087916 | |||
| 6949f1328c | |||
| b63090c20f | |||
| b15ebda948 | |||
| 252eef2e5e | |||
| 3bea8f9706 | |||
| dfe808bee7 | |||
| 58f3dd5336 | |||
| b00f16648f | |||
| fc66c31e89 | |||
| a26c849532 | |||
| d2991e60b6 | |||
| 10ba1430f9 | |||
| 6d71f24f75 | |||
| 8f4d20e411 | |||
| cf758d773e | |||
| c012f0c4c5 | |||
| 9e85900057 | |||
| 68e3b787f9 | |||
| 6706704ad6 | |||
| 1067fa015e | |||
| 411335ebcb | |||
| f19c15491e | |||
| 0da81a4d10 | |||
| 049839a5c7 | |||
| a901927844 | |||
| 07ab0bc5a1 | |||
| a6025e6fab | |||
| e841a61bf0 | |||
| 2f1c2110c6 | |||
| 8e573eea4c | |||
| 149765764e | |||
| 45d6444c7e | |||
| aa65f49190 | |||
| 1bda41ff67 | |||
| 5a799139cd | |||
| eb36d0742a | |||
| d8a9123852 | |||
| 19031c2197 | |||
| 7a2ffdf39c | |||
| ce8fa79206 | |||
| b05c63549d | |||
| 9f98660423 | |||
| 7b0fa862d2 | |||
| bfe2d4d573 | |||
| 26950c5673 | |||
| 86788362a5 | |||
| deedc5fb2e | |||
| f4fbe67db9 | |||
| 599ce0eade | |||
| 11f3ab8dc7 | |||
| 75b4a6dd46 | |||
| 86bbea337d | |||
| e63a30064b | |||
| 222b1ddbd9 | |||
| a88dd88a07 | |||
| a47c01fd41 | |||
| 35e385f7a7 | |||
| 191e3b7d2c | |||
| 13fbbc190a | |||
| 61c6a6e7f3 | |||
| 911f218f0b | |||
| af41350eb9 | |||
| 8f63e348b3 | |||
| 8901cf8e81 | |||
| e11736e1d2 | |||
| c062fba69c | |||
| 4628095fe7 | |||
| 787ed9bc0f | |||
| daaff8974e | |||
| 4fcbd511c0 | |||
| 64d6bff6d9 | |||
| 998cbace92 | |||
| 2204038951 | |||
| 998f67ac5d | |||
| be981a2c9b | |||
| da3cf0d4a3 | |||
| 7124b9a36c | |||
| 712a6b6c39 | |||
| ed0a1c57d6 | |||
| 7858228505 | |||
| 2525a22d78 | |||
| 7b4ef8fc31 | |||
| ce14cd02e4 | |||
| 05f501af52 | |||
| e94fa4844f | |||
| e0965aae5e | |||
| 414c1de963 | |||
| b589102be8 | |||
| df367e0d47 | |||
| 680219ebcb | |||
| ef87487f60 | |||
| 8bafe72434 | |||
| 76dcc69e44 | |||
| 682ef22194 | |||
| ee8dd41605 | |||
| d0b8d666e4 | |||
| 4a81826d19 | |||
| 848a10c3a8 | |||
| a4baa29678 | |||
| a9b5280691 | |||
| 264706210b | |||
| 7677ae254f | |||
| 7ccb4c5f06 | |||
| 01205af018 | |||
| 5b5150e6d4 | |||
| 9d8e3f5049 | |||
| 99ede83bdc | |||
| 61488e840d | |||
| a80d01209c | |||
| d2a8d655c8 | |||
| 80e2b34abc | |||
| fb3d43d2d5 | |||
| ebdec4fa44 | |||
| ead8dbbaa5 | |||
| 95615a5501 | |||
| 75487b1f58 | |||
| c7daaa00cd | |||
| 606f24e981 | |||
| c83d9287a7 | |||
| fa8092b2cd | |||
| b2b07a2be6 | |||
| a21628e350 | |||
| 9fcd686fda | |||
| 1759fd4cf9 | |||
| 12e7837602 | |||
| ff349f2edf | |||
| 088d8ca207 | |||
| c37902d3a4 | |||
| da38684cdd | |||
| cb2cb72333 | |||
| d53c8f9830 | |||
| c8136d4231 | |||
| e4b8380530 | |||
| fbd599194c | |||
| 0270ec26fb | |||
| 79e342be57 | |||
| c89f23b018 | |||
| f971ec5d34 | |||
| c5776ce41f | |||
| 75c5ebbffa | |||
| d51a724ade | |||
| c1143d7a6d | |||
| f3697431a4 | |||
| e3d6e5f420 | |||
| af6f7a7146 | |||
| c18d95e89a | |||
| fa42d03a65 | |||
| 22f7de2c09 | |||
| b4b9df81cb | |||
| 2a71c2b0e7 | |||
| c37b3d3df5 | |||
| ae135b89d6 | |||
| 4112426848 | |||
| 638b399b31 | |||
| 8a5713ef7c | |||
| 382d7bfaa1 | |||
| 3daf2f7738 | |||
| 944d35971d | |||
| 9ce8af0b82 | |||
| d825b9ec26 | |||
| 9afe4aea4b | |||
| 3cc8c3284a | |||
| e2c18c4e1e | |||
| 81fe768f6a | |||
| 8923779ab0 | |||
| 583218a045 | |||
| 153b2bfa53 | |||
| 0a9b325360 | |||
| af9e6f2c46 | |||
| 0e31726fd3 | |||
| 590d9c2e61 | |||
| c12fe9f25e | |||
| 9bd83f3acf | |||
| aada15fc41 | |||
| 6a2d383be6 | |||
| 545c8fa482 | |||
| e3d18643b8 | |||
| 2f318bbe9f | |||
| fac7eb61b3 | |||
| 4450d03a54 | |||
| e4e7868b30 | |||
| 3d7fa680cb | |||
| c38035e25e | |||
| 8d4fdaf902 | |||
| 2820adad53 | |||
| f7a427d2c0 | |||
| 59ed8c9660 | |||
| 71562ab0e5 | |||
| 278ddf037f | |||
| ac3151af92 | |||
| 1189ad7862 | |||
| 88c13e7b9a | |||
| 596baac62e | |||
| 5a8503ec14 | |||
| e44f95724d | |||
| dec0d0dea7 | |||
| 46f96e94ec | |||
| 89b30bcf58 | |||
| 473d8f5f85 | |||
| 08d8d65599 | |||
| 2964c1d82e | |||
| 75ab8552d9 | |||
| b78d5b3d03 | |||
| 064081c771 | |||
| 3525d35d15 | |||
| 9d9ee7c585 | |||
| e65c32c4c7 | |||
| 113769791f | |||
| 5d2a1d21d5 | |||
| 6e40f92aaf | |||
| 302cb8a5be | |||
| 0b27890484 | |||
| 5db247e632 | |||
| c1155a4338 | |||
| da806b9492 | |||
| 3d6d46b30d | |||
| 57c69738ba | |||
| 0c8157dbc0 | |||
| d7b278f2f7 | |||
| 18d74f1057 | |||
| e04856f794 | |||
| d7bbfb0fc3 | |||
| a0857817de | |||
| 8d39234fd0 | |||
| 0fce1c0fd8 | |||
| c23c285f82 | |||
| 81b560d791 | |||
| 55459a4f42 | |||
| 9eafd3e6ca | |||
| e7ac26ff5a | |||
| fad78641d6 | |||
| a32cb0142b | |||
| 4a3c133152 | |||
| ae79c34508 | |||
| 7eb69c1ad7 | |||
| 8aaff0af89 | |||
| 4b77280c65 | |||
| a0df84e4f2 | |||
| 8876e6a098 | |||
| 9bee2a90f9 | |||
| cd6a811bbd | |||
| 84f9a83f55 | |||
| 2efc669ab2 | |||
| 5444ed77ad | |||
| dd71fe80a5 | |||
| b73092eb64 | |||
| aa28f87b0c | |||
| 1e1bcd4a30 | |||
| d24b3c46bf | |||
| 37222f07d9 | |||
| 49646a79e5 | |||
| 9940e1210e | |||
| 10892812c3 | |||
| d8ff5987dd | |||
| d014d418e9 | |||
| cc1b56501d | |||
| a5ad9648bf | |||
| 3630099234 | |||
| 941a24c75b | |||
| 50bdc01484 | |||
| d9e340ddc4 | |||
| beee1e91d6 | |||
| 35935d2bac | |||
| 5d0b0ad33e | |||
| d1a6ac531f | |||
| ae255a3bd9 | |||
| fa733afa51 | |||
| b60c723457 | |||
| 2bb7ff3c13 | |||
| 68b4cf00e2 | |||
| 440169ff60 | |||
| 1fc361242e | |||
| 72cc6f3d75 | |||
| a2b1924e00 | |||
| 7e926167f0 | |||
| 32484bd32e | |||
| 04ade1008b | |||
| 4603de0e70 | |||
| 12c1ff65e9 | |||
| 7b8cbf7f86 | |||
| c198e83f70 | |||
| e60863a290 | |||
| 946563d3b2 | |||
| 3f2ef1d54e | |||
| 0d2b60b905 | |||
| 161b11e428 | |||
| f7748d51df | |||
| 5d13b4b705 | |||
| 92e52a2284 | |||
| 3efd5fb77a | |||
| c3d62bb8d8 | |||
| c099e843d5 | |||
| 5f3a5871f1 | |||
| f01fdd0070 | |||
| d20cc367b8 | |||
| eadb9a733f | |||
| 32ac454b5b | |||
| 2cadd6af44 | |||
| 3c8b5cb313 | |||
| fcf2387794 | |||
| f9783b4806 | |||
| cbdc532f83 | |||
| 124554bae5 | |||
| 2725536d7d | |||
| beeef7a4f7 | |||
| 7f2ebb6aeb | |||
| c8e30383ba | |||
| a7d5a6ccb9 | |||
| be3380aaf3 | |||
| 40a4ab5410 | |||
| 54a2181960 | |||
| e490e15bc4 | |||
| 469844d97b | |||
| 892ea29ba8 | |||
| acde1c6742 | |||
| 07da11d852 | |||
| 502b8c2270 | |||
| af3f7ac810 | |||
| 935c6caf96 | |||
| 90bce1d437 | |||
| 831dd3e2e0 | |||
| b8e208eb32 | |||
| 542c908e30 | |||
| 08529242bf | |||
| 1ae9449d92 | |||
| 7fd0b1fa08 | |||
| 111572e3f2 | |||
| c9875d24b4 | |||
| 94a75603c3 | |||
| d55925bccd | |||
| 0b5adcbfab | |||
| 8d84562f32 | |||
| 136d159833 | |||
| e3026062d8 | |||
| 8a69c69b7f | |||
| 197714a57a | |||
| 567cdc6f8d | |||
| be6e8c7713 | |||
| f667b35403 | |||
| 942e482ca4 | |||
| 10b381965e | |||
| 79c3a99a38 | |||
| 463d3c8e97 | |||
| da007729c6 | |||
| b7b42b5fd4 | |||
| 735f9bd053 | |||
| 6096b65374 | |||
| 727d342eb3 | |||
| 58815ba8bc | |||
| 49d0502775 | |||
| 0b6c6e6d2c | |||
| 45ede6047a | |||
| 8f469d4ebb | |||
| f3264d056d |
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: frontend-developer
|
||||
description: Use this agent when you need assistance with frontend development tasks including Vue.js components, UI implementation, styling, responsive design, state management, or frontend architecture decisions. Examples: <example>Context: User is working on a Vue.js component and needs help with implementing a responsive layout. user: 'I need to create a mobile-friendly chat interface component' assistant: 'I'll use the frontend-developer agent to help design and implement this responsive chat component' <commentary>Since this involves frontend development work with Vue.js and responsive design, use the frontend-developer agent.</commentary></example> <example>Context: User encounters styling issues with Element Plus components. user: 'The Element Plus dialog is not displaying correctly on mobile devices' assistant: 'Let me use the frontend-developer agent to troubleshoot this mobile styling issue' <commentary>This is a frontend styling problem that requires expertise in Element Plus and responsive design.</commentary></example>
|
||||
color: purple
|
||||
---
|
||||
|
||||
You are a Senior Frontend Development Engineer with deep expertise in modern web development technologies, particularly Vue.js 3, Element Plus, Vant, and responsive design patterns. You specialize in creating high-quality, maintainable frontend applications with excellent user experience.
|
||||
|
||||
Your core responsibilities include:
|
||||
- Developing Vue.js 3 components using Composition API and best practices
|
||||
- Implementing responsive designs that work seamlessly across desktop and mobile devices
|
||||
- Working with Element Plus for desktop UI and Vant for mobile components
|
||||
- Managing application state using Pinia store patterns
|
||||
- Styling with Stylus preprocessor and Tailwind CSS utilities
|
||||
- Optimizing build processes with Vite and ensuring proper code organization
|
||||
- Implementing theme switching (dark/light mode) and accessibility features
|
||||
- Follow decoupled development, with HTML, CSS, and JS codes placed in separate files for easier maintenance
|
||||
|
||||
When working on frontend tasks, you will:
|
||||
1. Analyze requirements and suggest the most appropriate Vue.js patterns and component structures
|
||||
2. Ensure responsive design principles are followed, considering both desktop and mobile viewports
|
||||
3. Choose appropriate UI components from Element Plus (desktop) or Vant (mobile) libraries
|
||||
4. Write clean, maintainable code following Vue.js 3 Composition API best practices
|
||||
5. Consider performance implications and suggest optimizations when relevant
|
||||
6. Ensure proper state management using Pinia when component state needs to be shared
|
||||
7. Follow the project's established patterns for routing, API integration, and component organization
|
||||
8. Provide specific code examples and explain the reasoning behind architectural decisions
|
||||
|
||||
You have deep knowledge of:
|
||||
- Vue.js 3 ecosystem (Vue Router, Pinia, Composition API)
|
||||
- Modern CSS techniques and preprocessors (Stylus, Tailwind)
|
||||
- Component library integration (Element Plus, Vant)
|
||||
- Build tools and development workflow (Vite, npm scripts)
|
||||
- Cross-browser compatibility and mobile-first design principles
|
||||
- Performance optimization and code splitting strategies
|
||||
|
||||
Always consider the user experience, code maintainability, and alignment with modern frontend development standards. When suggesting solutions, provide clear explanations and consider both immediate needs and long-term scalability.
|
||||
@@ -0,0 +1,6 @@
|
||||
重构当前页面代码
|
||||
|
||||
1. 把当前页面 JS 代码全部抽离,然后是采用 Pinia 重构
|
||||
2. 把当前页面 CSS 代码全部抽离,如果是 stylus 语法代码,则需要改成 SCSS 语法代码
|
||||
3. 尽量做到代码的复用性,不要重复造轮子
|
||||
4. 移动端的 css 和 js 分别放到对应的 mobile 目录下,不要覆盖 PC 端的代码
|
||||
@@ -0,0 +1,226 @@
|
||||
# ui-ux-pro-max
|
||||
|
||||
Searchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Check if Python is installed:
|
||||
|
||||
```bash
|
||||
python3 --version || python --version
|
||||
```
|
||||
|
||||
If Python is not installed, install it based on user's OS:
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install python3
|
||||
```
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt update && sudo apt install python3
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
winget install Python.Python.3.12
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Workflow
|
||||
|
||||
When user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:
|
||||
|
||||
### Step 1: Analyze User Requirements
|
||||
|
||||
Extract key information from user request:
|
||||
- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.
|
||||
- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.
|
||||
- **Industry**: healthcare, fintech, gaming, education, etc.
|
||||
- **Stack**: React, Vue, Next.js, or default to `html-tailwind`
|
||||
|
||||
### Step 2: Search Relevant Domains
|
||||
|
||||
Use `search.py` multiple times to gather comprehensive information. Search until you have enough context.
|
||||
|
||||
```bash
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n <max_results>]
|
||||
```
|
||||
|
||||
**Recommended search order:**
|
||||
|
||||
1. **Product** - Get style recommendations for product type
|
||||
2. **Style** - Get detailed style guide (colors, effects, frameworks)
|
||||
3. **Typography** - Get font pairings with Google Fonts imports
|
||||
4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)
|
||||
5. **Landing** - Get page structure (if landing page)
|
||||
6. **Chart** - Get chart recommendations (if dashboard/analytics)
|
||||
7. **UX** - Get best practices and anti-patterns
|
||||
8. **Stack** - Get stack-specific guidelines (default: html-tailwind)
|
||||
|
||||
### Step 3: Stack Guidelines (Default: html-tailwind)
|
||||
|
||||
If user doesn't specify a stack, **default to `html-tailwind`**.
|
||||
|
||||
```bash
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "<keyword>" --stack html-tailwind
|
||||
```
|
||||
|
||||
Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`
|
||||
|
||||
---
|
||||
|
||||
## Search Reference
|
||||
|
||||
### Available Domains
|
||||
|
||||
| Domain | Use For | Example Keywords |
|
||||
|--------|---------|------------------|
|
||||
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
|
||||
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
|
||||
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
|
||||
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
|
||||
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
|
||||
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
|
||||
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
|
||||
| `prompt` | AI prompts, CSS keywords | (style name) |
|
||||
|
||||
### Available Stacks
|
||||
|
||||
| Stack | Focus |
|
||||
|-------|-------|
|
||||
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
|
||||
| `react` | State, hooks, performance, patterns |
|
||||
| `nextjs` | SSR, routing, images, API routes |
|
||||
| `vue` | Composition API, Pinia, Vue Router |
|
||||
| `svelte` | Runes, stores, SvelteKit |
|
||||
| `swiftui` | Views, State, Navigation, Animation |
|
||||
| `react-native` | Components, Navigation, Lists |
|
||||
| `flutter` | Widgets, State, Layout, Theming |
|
||||
|
||||
---
|
||||
|
||||
## Example Workflow
|
||||
|
||||
**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp"
|
||||
|
||||
**AI should:**
|
||||
|
||||
```bash
|
||||
# 1. Search product type
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --domain product
|
||||
|
||||
# 2. Search style (based on industry: beauty, elegant)
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "elegant minimal soft" --domain style
|
||||
|
||||
# 3. Search typography
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "elegant luxury" --domain typography
|
||||
|
||||
# 4. Search color palette
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --domain color
|
||||
|
||||
# 5. Search landing page structure
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "hero-centric social-proof" --domain landing
|
||||
|
||||
# 6. Search UX guidelines
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "animation" --domain ux
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "accessibility" --domain ux
|
||||
|
||||
# 7. Search stack guidelines (default: html-tailwind)
|
||||
python3 .shared/ui-ux-pro-max/scripts/search.py "layout responsive" --stack html-tailwind
|
||||
```
|
||||
|
||||
**Then:** Synthesize all search results and implement the design.
|
||||
|
||||
---
|
||||
|
||||
## Tips for Better Results
|
||||
|
||||
1. **Be specific with keywords** - "healthcare SaaS dashboard" > "app"
|
||||
2. **Search multiple times** - Different keywords reveal different insights
|
||||
3. **Combine domains** - Style + Typography + Color = Complete design system
|
||||
4. **Always check UX** - Search "animation", "z-index", "accessibility" for common issues
|
||||
5. **Use stack flag** - Get implementation-specific best practices
|
||||
6. **Iterate** - If first search doesn't match, try different keywords
|
||||
7. **Split Into Multiple Files** - For better maintainability:
|
||||
- Separate components into individual files (e.g., `Header.tsx`, `Footer.tsx`)
|
||||
- Extract reusable styles into dedicated files
|
||||
- Keep each file focused and under 200-300 lines
|
||||
|
||||
---
|
||||
|
||||
## Common Rules for Professional UI
|
||||
|
||||
These are frequently overlooked issues that make UI look unprofessional:
|
||||
|
||||
### Icons & Visual Elements
|
||||
|
||||
| Rule | Do | Don't |
|
||||
|------|----|----- |
|
||||
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |
|
||||
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
|
||||
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
|
||||
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
|
||||
|
||||
### Interaction & Cursor
|
||||
|
||||
| Rule | Do | Don't |
|
||||
|------|----|----- |
|
||||
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
|
||||
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
|
||||
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
|
||||
|
||||
### Light/Dark Mode Contrast
|
||||
|
||||
| Rule | Do | Don't |
|
||||
|------|----|----- |
|
||||
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
|
||||
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
|
||||
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
|
||||
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
|
||||
|
||||
### Layout & Spacing
|
||||
|
||||
| Rule | Do | Don't |
|
||||
|------|----|----- |
|
||||
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
|
||||
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
|
||||
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
|
||||
|
||||
---
|
||||
|
||||
## Pre-Delivery Checklist
|
||||
|
||||
Before delivering UI code, verify these items:
|
||||
|
||||
### Visual Quality
|
||||
- [ ] No emojis used as icons (use SVG instead)
|
||||
- [ ] All icons from consistent icon set (Heroicons/Lucide)
|
||||
- [ ] Brand logos are correct (verified from Simple Icons)
|
||||
- [ ] Hover states don't cause layout shift
|
||||
|
||||
### Interaction
|
||||
- [ ] All clickable elements have `cursor-pointer`
|
||||
- [ ] Hover states provide clear visual feedback
|
||||
- [ ] Transitions are smooth (150-300ms)
|
||||
- [ ] Focus states visible for keyboard navigation
|
||||
|
||||
### Light/Dark Mode
|
||||
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
|
||||
- [ ] Glass/transparent elements visible in light mode
|
||||
- [ ] Borders visible in both modes
|
||||
- [ ] Test both modes before delivery
|
||||
|
||||
### Layout
|
||||
- [ ] Floating elements have proper spacing from edges
|
||||
- [ ] No content hidden behind fixed navbars
|
||||
- [ ] Responsive at 320px, 768px, 1024px, 1440px
|
||||
- [ ] No horizontal scroll on mobile
|
||||
|
||||
### Accessibility
|
||||
- [ ] All images have alt text
|
||||
- [ ] Form inputs have labels
|
||||
- [ ] Color is not the only indicator
|
||||
- [ ] `prefers-reduced-motion` respected
|
||||
@@ -0,0 +1,5 @@
|
||||
web/src/assets/fonts filter=lfs diff=lfs merge=lfs -text
|
||||
web/src/assets/iconfont filter=lfs diff=lfs merge=lfs -text
|
||||
web/src/assets/img filter=lfs diff=lfs merge=lfs -text
|
||||
web/public filter=lfs diff=lfs merge=lfs -text
|
||||
api/res filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -14,3 +14,7 @@ logs
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
miniprogram
|
||||
.shared
|
||||
.claude
|
||||
docs/mydocs
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## 项目结构与模块组织
|
||||
- `api/`:Go + Gin 后端,包含 `core/` 业务、`handler/` 控制器、`service/` 接口调用以及 `store/` 数据访问,`config.toml` 存放默认配置,`Makefile` 用于多架构交叉构建。
|
||||
- `web/`:Vue3 + Vite 前端,源码集中在 `src/`,`public/` 为静态资源,`dist/` 存放构建结果并可被 `api/static/` 或 `desktop/` 引用。
|
||||
- `desktop/`:Electron 客户端入口为 `index.js`,配合 `electron-builder` 可打包 AppImage/DMG/NSIS。
|
||||
- `miniprogram/`、`docs/`、`database/` 与 `config/` 分别承载小程序壳、部署文档、SQL 脚本及全局 YAML 配置;`build/` 包含 Dockerfile、安装脚本。
|
||||
|
||||
## 构建、测试与开发命令
|
||||
- `docker-compose up -d`:根目录拉起全部容器,需提前准备好 MySQL、Redis 与模型密钥。
|
||||
- `cd api && go run main.go`:本地热调试;`make amd64` / `make arm64` 生成无 CGO 二进制至 `api/bin/` 便于镜像打包。
|
||||
- `cd web && pnpm install && pnpm dev --host`:Vite 开发模式;`pnpm build` 产出静态文件;`pnpm lint` 运行 ESLint 自动修复。
|
||||
- `cd desktop && npm install && npm run start`:调试 Electron;`npm run package` 通过 electron-builder 生成多平台安装包。
|
||||
|
||||
## 编码风格与命名规范
|
||||
- Go 代码必须经过 `gofmt`/`goimports`,保持 tab 缩进与驼峰命名;HTTP 路由遵循 `/api/v1/resources` 模式,与 handler 函数命名 (`ResourceHandler`) 对应。
|
||||
- Vue 组件文件使用 PascalCase(如 `ChatPanel.vue`),Pinia store 与工具采用 kebab-case 文件名(如 `chat-session.ts`);统一通过 ESLint、Tailwind 与 `postcss.config.js` 约束样式。
|
||||
|
||||
## 测试指南
|
||||
- `cd api && go test ./... -race` 是最低要求,新增 service/handler 需补 `_test.go` 并用 mock 隔离第三方 API;涉及时序逻辑可新增 `Test*Integration` 验证。
|
||||
- 前端暂未启用单测框架,至少运行 `pnpm lint` 并在 PR 中附关键页面截图或录屏证明交互可用;桌面端如修改构建脚本,需在 macOS/Linux/Windows 中至少验证一个安装包。
|
||||
|
||||
## 提交与 Pull Request 规范
|
||||
- 参考历史记录(如“支持腾讯云短信服务”),提交信息使用中文动词开头、聚焦单一变更,并可加子系统前缀:`web: 优化聊天动画`。
|
||||
- PR 描述需包含变更背景、实现概述、验证方式(命令、截图或日志)与关联 issue/任务号;涉及配置或部署脚本,还要说明回滚流程并 @ 相关 reviewer。
|
||||
|
||||
## 安全与配置提示
|
||||
- 禁止提交真实密钥,请复制 `config.sample.toml` 或 `config/config.yaml` 生成私有文件,并用 `git update-index --skip-worktree` 忽略。
|
||||
- 对象存储、短信、支付等凭证统一放入 Vault 或 CI Secret,代码中仅引用占位常量;`docs/` 中同步记录新增敏感字段与启用步骤。
|
||||
@@ -1,5 +1,84 @@
|
||||
# 更新日志
|
||||
|
||||
## v4.3.0
|
||||
|
||||
- 功能新增:**PPT 生成功能**,复刻 NotebookLLM 的演示文稿生成功能,支持编辑和导出 🔥🔥🔥
|
||||
- 功能新增:AI 绘图与视频创作提示词支持 @ 引用图片 🔥🔥🔥
|
||||
- 功能新增:接入豆包视频生成,优化 Sora 图生视频体验 🔥🔥🔥
|
||||
- 功能重构:移除 Stable Diffusion(SD)生图模块及相关前后端代码;数据库表 `geekai_sd_jobs` 保留不删除;管理后台算力配置移除 SD、可灵、Luma 等相关算力项
|
||||
- 功能重构:图片生成模块从 dalle 重命名为 image,支持查看任务详情 🔥🔥🔥
|
||||
- 功能优化:任务详情弹窗优化并补充图片任务创建时间
|
||||
- 功能优化:图片生成页尺寸支持自定义输入与预设选择;优化比例与尺寸参数展示
|
||||
- 功能优化:优化上传组件,支持拖拽上传;更新 iconfont;图片接口改造以兼容 kapon 聚合服务
|
||||
- 功能优化:MJ 支持局部重绘与算力配置优化;任务列表返回创建时间
|
||||
- 功能重构:重构 Gem(智能体)模块,调整智能体标识与工作区存储
|
||||
- 功能优化:优化 ChatPlus 欢迎页与会话输入体验,与 ImageMj 交互及 UI 一致性
|
||||
- 功能优化:支持批量导入用户信息,优化管理后台 UI 样式
|
||||
- 功能优化:重构管理后台 UI,采用现代科技风格
|
||||
- 功能优化:优化左侧菜单 UI,增加选中菜单对比度
|
||||
- Bug修复:完善数据迁移中各表自增主键及用户表主键迁移;迁移时清理历史 Dalle 与 SD 相关数据
|
||||
|
||||
## v4.2.9
|
||||
|
||||
- 功能优化:支持缩略图功能配置,支持本地存储,七牛云,腾讯云 OSS 和阿里云 OSS 四种存储介质。
|
||||
- 功能优化:重构视频生成模块,支持 Sora2, Veo3.1, Luma, 可灵,通义万相,MiniMax 等视频生成模型。
|
||||
- 功能新增:腾讯云短信服务 🔥🔥🔥
|
||||
- 功能新增:支持腾讯云 OSS 存储,支持腾讯云 COS 文件上传和下载 🔥🔥🔥
|
||||
- 功能新增:支持微信支付原生的 JSAPI 支付,支持微信公众号授权登录。🔥🔥🔥
|
||||
- 功能优化:对话页面输入框支持粘贴剪切板内容上传截图 🔥🔥🔥
|
||||
- 功能优化:给 Sora2 生成视频下载增加重试机制,防止因为网络不稳定导致下载失败 🎉🎉🎉
|
||||
- Bug 修复:删除即梦 4.0 生图不支持的分片率参数,支持 1K, 2K, 4K 分辨率
|
||||
- 功能优化:即梦 AI 新增 3.0 电商营销产品背景替换功能 🔥🔥🔥
|
||||
- Bug 修复:调整 PC 端会员页面的列表样式,重构页面布局,移除多余的 css 样式,采用 tailwindcss 样式
|
||||
- Bug 修复:修复管理后台重置管理密码不生效的问题
|
||||
- Bug 修复:修复 Chat 页面输入框输入内容会自动滚动到最底部的问题
|
||||
- Bug 修复:修复即梦视频生成任务 duration(视频时长) 参数不生效的问题 🎉🎉🎉
|
||||
- Bug 修复:修复管理后台系统配置邮箱白名单更改不生效问题
|
||||
- Bug 修复:修复聊天页面角色列表显示 system prompt 提示词的问题
|
||||
|
||||
## v4.2.8
|
||||
|
||||
- Bug 修复:修复管理后台邮件配置报“参数错误”问题
|
||||
- 功能优化:手机端也不显示后端禁用的菜单功能页面
|
||||
- 功能优化:**聊天页面支持 Sora2 文生视频/图生视频功能** 🔥🔥🔥
|
||||
- 功能优化:**重构路由代码,优化路由加载逻辑,支持关闭手机站功能** 🔥🔥🔥
|
||||
- Bug 修复:修复用户算力扣减日志记录逻辑错误问题
|
||||
- 功能优化:优化 404 页面样式,兼容 PC 端和移动端
|
||||
- Bug 修复:ipv6 地址登录报错:Data too long for column 'last_login_ip'
|
||||
- Bug 修复:移动端登录页面输入密码的时候会覆盖在确认按钮上层
|
||||
- 功能优化:**移动端的 DALL-E 绘图页面支持上传参考图** 🔥🔥🔥
|
||||
- 功能优化:优化 WebFooter 组件,如果配置了备案号,则显示备案号,否则不显示
|
||||
|
||||
## v4.2.7
|
||||
|
||||
- Bug 修复:修复超级管理员无法修改密码的 Bug
|
||||
- Bug 修复:微信登录配置更新后,没有同步更新到系统配置
|
||||
- 功能优化: 给 AI 对话 API 加上线程锁,确保同一个用户同时只有一个对话请求
|
||||
- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能。🔥🔥🔥
|
||||
- 功能新增:新增 AI 对话编辑功能,并优化了重新生成逻辑
|
||||
|
||||
## v4.2.6
|
||||
|
||||
- 功能重构:优化系统配置管理功能,把 OSS,支付,短信,邮件等配置全部迁移到管理后台,无需通过修改配置文档的方式修改 🎉🎉🎉
|
||||
- 功能优化:重构 API 授权代码,采用中间件鉴权方式,实现更加精准的 API 鉴权 🎉🎉🎉
|
||||
- 功能优化:优化 PC 端的 Suno 音乐,视频生成,以及即梦 AI 页面 UI
|
||||
- 功能优化:重构登录和注册页面,兼容移动端和 PC 端,并且所有的登录组件共用了同一套组件代码,大大降低维护成本 🎉🎉🎉
|
||||
- 功能优化:管理后台增加模型批量删除功能
|
||||
- 功能优化:优化 Table 组件 UI,并支持 dark 主题
|
||||
- 功能优化:移动端对话页面支持上传文件和图片
|
||||
- 功能新增:新增微信扫码登录支持
|
||||
- 功能新增:新增安全监控,内容审核功能,支持敏感内容过滤拦截
|
||||
- 功能新增:DALL-E 绘图支持参 Google Banana 图片编辑功能
|
||||
|
||||
## v4.2.5
|
||||
|
||||
- 功能优化:在代码右下角增加复制代码功能按钮,增加收起和展开代码功能
|
||||
- Bug 修复:修复 Shift + Enter 不换行的 Bug
|
||||
- Bug 修复:修复管理后台菜单添加页面的文本错误
|
||||
- Bug 修复:解决聊天页面异常退出不断重连的 bug
|
||||
- 功能优化:把 Luma 和可灵视频生成页面整合成一个视频创作中心页面,统一管理视频任务
|
||||
- 功能新增:增加即梦 AI 专题页面,支持即梦官方原生 API 的图片和视频生成 🎉🎉🎉
|
||||
|
||||
## v4.2.4
|
||||
|
||||
- 功能优化:更改前端构建技术选型,使用 Vite 构建,提升构建速度和兼容性
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Go Backend (api/)
|
||||
- **Development**: `cd api && go run main.go` (uses config.toml)
|
||||
- **Build**: `cd api && make` (builds both amd64 and arm64 binaries)
|
||||
- **Individual builds**: `make amd64` or `make arm64`
|
||||
- **Clean**: `make clean`
|
||||
- **Config**: Copy `config.sample.toml` to `config.toml` and configure
|
||||
|
||||
### Web Frontend (web/)
|
||||
- **Development**: `cd web && npm run dev` (runs on Vite dev server with --host)
|
||||
- **Build**: `cd web && npm run build`
|
||||
- **Lint**: `cd web && npm run lint` (ESLint with auto-fix)
|
||||
|
||||
### Testing
|
||||
- Backend tests: `cd api/test && bash run_crawler_test.sh`
|
||||
- No specific frontend test configuration found
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Backend (Go)
|
||||
- **Framework**: Gin web framework with dependency injection via uber-go/fx
|
||||
- **Database**: GORM with MySQL, Redis for caching, LevelDB for local storage
|
||||
- **Authentication**: JWT tokens with Redis session storage
|
||||
- **Middleware**: CORS, authorization, parameter handling, static resource serving
|
||||
- **Structure**:
|
||||
- `handler/`: HTTP request handlers (REST API endpoints)
|
||||
- `service/`: Business logic services (AI integrations, payments, etc.)
|
||||
- `store/`: Database models and data access layer
|
||||
- `core/`: Application server and middleware configuration
|
||||
- `utils/`: Utility functions and helpers
|
||||
|
||||
### Frontend (Vue.js)
|
||||
- **Framework**: Vue 3 with Composition API
|
||||
- **UI Components**: Element Plus + Vant (mobile components)
|
||||
- **State Management**: Pinia
|
||||
- **Routing**: Vue Router with nested routes
|
||||
- **Build Tool**: Vite
|
||||
- **CSS**: Stylus preprocessor with Tailwind CSS utilities
|
||||
- **Features**: Responsive design (desktop/mobile views), theme switching (dark/light)
|
||||
|
||||
### Key Features
|
||||
- **AI Chat**: Multiple chat models and conversation management
|
||||
- **Image Generation**: MidJourney, Stable Diffusion, DALL-E integration
|
||||
- **Audio/Video**: Suno music creation, Luma/KeLing video generation
|
||||
- **User Management**: Authentication, payments, power logs, invitations
|
||||
- **Admin Panel**: Comprehensive management interface
|
||||
|
||||
### Database Models
|
||||
Key entities: User, ChatItem, ChatMessage, ChatRole, ChatModel, Order, Product, AdminUser, and various job types for AI services.
|
||||
|
||||
### API Structure
|
||||
- User APIs: `/api/user/*` (auth, profile, settings)
|
||||
- Chat APIs: `/api/chat/*` (conversations, messages)
|
||||
- AI Service APIs: `/api/mj/*`, `/api/dall/*`, `/api/suno/*`, `/api/video/*`
|
||||
- Admin APIs: `/api/admin/*` (management functions)
|
||||
|
||||
### Configuration
|
||||
- Backend: TOML configuration file (`config.toml`)
|
||||
- Database: MySQL with automatic migrations
|
||||
- Services: Redis, various AI API integrations
|
||||
- File Storage: Local, Aliyun OSS, MinIO, Qiniu options
|
||||
@@ -0,0 +1,8 @@
|
||||
# 项目固定指令(Cursor 必须遵守)
|
||||
|
||||
1. 代码必须使用清晰命名,禁止单字母变量
|
||||
2. 代码必须加注释
|
||||
3. 统一使用 4 空格缩进
|
||||
4. 前端样式代码永远优先适应 Tailwind CSS 的样式, 别自定义样式。除非 Tailwind CSS 不支持或者做不到。
|
||||
5. 不要生成多余代码,保持简洁
|
||||
6. 每次生成代码前遵守以上规则
|
||||
@@ -1,13 +1,67 @@
|
||||
# GeekAI-PLUS
|
||||
基于 GeekAI 项目开发的高级版,增加了很多高级功能,比如思维导图,Dalle 绘画等。**高级版源码不会一次性开放,只提供镜像给大家免费使用**,源码会逐步逐步按照版同步迁移到[社区版(GeekAI)](https://github.com/yangjian102621/geekai)。所以如果大家想要二次开发,请移步去社区版。
|
||||
# 🚀 GeekAI:一站式 AI 创意生产力平台
|
||||
|
||||
**让创意触手可及,让创作变得简单**
|
||||
|
||||
一个功能完整、开箱即用的 AI 多模态内容创作平台,集成了对话、绘画、音乐、视频、思维导图等全链路 AI 创作能力。无论是个人创作者还是企业团队,都能快速搭建属于自己的 AI 创作工作台。
|
||||
|
||||
## ✨ 核心特色
|
||||
|
||||
### 🎨 **全能 AI 创作矩阵**
|
||||
|
||||
- **智能对话**:集成 ChatGPT、Claude 等多款顶级 AI 模型,支持角色扮演和专业对话
|
||||
- **图像生成**:整合 MidJourney、DALL-E、Nano-Banana,即梦,可灵等主流 AI 绘画引擎
|
||||
- **音频创作**:Suno AI 音乐生成,从旋律到歌词一键创作专属音乐
|
||||
- **视频制作**:Luma 和 KeLing,即梦,Veo3 视频 AI,文本到视频,创意无限
|
||||
- **思维导图**:AI 辅助思维整理,复杂想法可视化呈现
|
||||
|
||||
### 🏗️ **企业级技术架构**
|
||||
|
||||
- **高性能后端**:Go + Gin + MySQL + Redis,支持高并发访问
|
||||
- **现代化前端**:Vue3 + Element Plus + Vant,桌面移动双端适配
|
||||
- **智能缓存**:多层缓存策略,响应速度提升 80%
|
||||
- **弹性部署**:Docker 容器化部署,一键启动,轻松扩展
|
||||
- **私有化部署**:支持私有化部署,私有化部署不支持升级,需要手动升级
|
||||
- **文档支持**:丰富且详细的部署和 API 开发文档支持,二次开发轻松上手
|
||||
|
||||
### 💼 **商业化就绪**
|
||||
|
||||
- **完整用户系统**:注册登录、权限管理、积分充值
|
||||
- **灵活计费模式**:支持按次付费、包月订阅等多种商业模式
|
||||
- **数据统计分析**:用户行为、消费记录、系统性能全方位监控
|
||||
- **管理后台**:功能完备的管理员界面,运营数据一目了然
|
||||
|
||||
### 🎯 **用户体验优势**
|
||||
|
||||
- **响应式设计**:完美适配桌面、平板、手机等全终端设备
|
||||
- **暗黑模式**:支持明暗主题切换,护眼舒适
|
||||
- **实时交互**:流式响应,创作过程流畅无卡顿
|
||||
- **文件管理**:支持七牛云,阿里云 OSS,腾讯云 OSS,Minio 等多种云存储,作品安全可靠
|
||||
|
||||
## 🎪 **应用场景**
|
||||
|
||||
- **内容创作者**:博客写作、社交媒体素材、短视频制作
|
||||
- **企业营销**:品牌宣传材料、产品介绍、创意广告
|
||||
- **教育培训**:课件制作、知识图谱、互动内容
|
||||
- **个人娱乐**:AI 聊天、创意绘画、音乐创作
|
||||
|
||||
## 🔥 **为什么选择 GeekAI?**
|
||||
|
||||
1. **技术领先**:集成当前最先进的 AI 技术,始终保持创新前沿
|
||||
2. **开箱即用**:完整的商业化解决方案,无需从零开发
|
||||
3. **高度定制**:模块化架构设计,支持个性化功能扩展
|
||||
4. **稳定可靠**:经过大量用户验证,性能稳定,安全可信
|
||||
5. **持续更新**:紧跟 AI 技术发展,功能持续迭代升级
|
||||
|
||||
## 演示站点
|
||||
[Geek-AI 创作系统](https://www.geekai.me)
|
||||
|
||||
[Geek-AI 创作系统](https://chat.geekai.me)
|
||||
|
||||
## 文档地址
|
||||
[Geek-AI 文档](https://www.geekai.me/docs/)
|
||||
|
||||
[Geek-AI 文档](https://docs.geekai.me)
|
||||
|
||||
## 部署
|
||||
|
||||
1. 安装 docker 和 docker-compose 程序,这个自行解决。
|
||||
2. 直接在项目根目录运行启动命令:
|
||||
```shell
|
||||
@@ -15,5 +69,9 @@
|
||||
```
|
||||
|
||||
## 功能截图
|
||||
|
||||
请参考 [GeekAI 项目介绍](https://docs.geekai.me/info/)。
|
||||
|
||||
---
|
||||
|
||||
_让 AI 成为你最强大的创作伙伴,开启无限创意可能!_
|
||||
|
||||
@@ -16,6 +16,7 @@ tmp
|
||||
bin
|
||||
data
|
||||
config.toml
|
||||
config.toml.*.bak
|
||||
static/upload
|
||||
static/audio
|
||||
storage.json
|
||||
|
||||
+1
-100
@@ -1,6 +1,6 @@
|
||||
Listen = "0.0.0.0:5678"
|
||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
MysqlDns = "root:password@tcp(127.0.0.1:3306)/geekai?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
StaticDir = "./static" # 静态资源的目录
|
||||
StaticUrl = "/static" # 静态资源访问 URL
|
||||
TikaHost = "http://tika:9998"
|
||||
@@ -14,102 +14,3 @@ TikaHost = "http://tika:9998"
|
||||
Port = 6379
|
||||
Password = ""
|
||||
DB = 0
|
||||
|
||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
||||
ApiURL = "https://sapi.geekai.me"
|
||||
AppId = ""
|
||||
Token = ""
|
||||
|
||||
|
||||
[SMS] # Sms 配置,用于发送短信
|
||||
Active = "Ali" # 当前启用的短信服务,默认使用阿里云
|
||||
[SMS.Bao]
|
||||
Username = ""
|
||||
Password = ""
|
||||
Domain = "api.smsbao.com"
|
||||
Sign = "【极客学长】"
|
||||
CodeTemplate = "您的验证码是{code}。5分钟有效,若非本人操作,请忽略本短信。"
|
||||
[SMS.Ali]
|
||||
AccessKey = ""
|
||||
AccessSecret = ""
|
||||
Product = "Dysmsapi"
|
||||
Domain = "dysmsapi.aliyuncs.com"
|
||||
Sign = ""
|
||||
CodeTempId = ""
|
||||
|
||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
||||
Active = "local" # 默认使用本地文件存储引擎
|
||||
[OSS.Local]
|
||||
BasePath = "./static/upload" # 本地文件上传根路径
|
||||
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件前缀 URL,线上需要把 localhost 替换成自己的实际域名或者IP
|
||||
[OSS.Minio]
|
||||
Endpoint = "" # 如 172.22.11.200:9000
|
||||
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
|
||||
AccessSecret = ""
|
||||
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
|
||||
UseSSL = false
|
||||
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
|
||||
[OSS.QiNiu] # 七牛云 OSS 配置
|
||||
Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
|
||||
AccessKey = ""
|
||||
AccessSecret = ""
|
||||
Bucket = ""
|
||||
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
|
||||
[OSS.AliYun]
|
||||
Endpoint = "oss-cn-hangzhou.aliyuncs.com"
|
||||
AccessKey = ""
|
||||
AccessSecret = ""
|
||||
Bucket = "chatgpt-plus"
|
||||
SubDir = ""
|
||||
Domain = ""
|
||||
|
||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||
Enabled = false # 是否启用 XXL JOB 服务
|
||||
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||
ExecutorPort = "9999" # 执行器服务端口
|
||||
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||
|
||||
[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接
|
||||
UseTls = false
|
||||
Host = "smtp.163.com"
|
||||
Port = 25
|
||||
AppName = "极客学长"
|
||||
From = "test@163.com" # 发件邮箱人地址
|
||||
Password = "" #邮箱 stmp 服务授权码
|
||||
|
||||
# 支付宝商户支付
|
||||
[AlipayConfig]
|
||||
Enabled = false # 启用支付宝支付通道
|
||||
SandBox = false # 是否启用沙盒模式
|
||||
UserId = "2088721020750581" # 商户ID
|
||||
AppId = "9021000131658023" # App Id
|
||||
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
|
||||
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||
|
||||
# 虎皮椒支付
|
||||
[HuPiPayConfig]
|
||||
Enabled = false
|
||||
AppId = ""
|
||||
AppSecret = ""
|
||||
ApiURL = "https://api.xunhupay.com"
|
||||
|
||||
# 微信商户支付
|
||||
[WechatPayConfig]
|
||||
Enabled = false
|
||||
AppId = "" # 商户应用ID
|
||||
MchId = "" # 商户号
|
||||
SerialNo = "" # API 证书序列号
|
||||
PrivateKey = "certs/alipay/privateKey.txt" # API 证书私钥文件路径,跟支付宝一样,把私钥文件拷贝到对应的路径,证书路径要映射到容器内
|
||||
ApiV3Key = "" # APIV3 私钥,这个是你自己在微信支付平台设置的
|
||||
|
||||
# 易支付
|
||||
[GeekPayConfig]
|
||||
Enabled = true
|
||||
AppId = "" # 商户ID
|
||||
PrivateKey = "" # 商户私钥
|
||||
ApiURL = "https://pay.geekai.cn"
|
||||
Methods = ["alipay", "wxpay", "qqpay", "jdpay", "douyin", "paypal"] # 支持的支付方式
|
||||
|
||||
+14
-363
@@ -8,29 +8,20 @@ package core
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"golang.org/x/image/webp"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -38,22 +29,23 @@ type AppServer struct {
|
||||
Config *types.AppConfig
|
||||
Engine *gin.Engine
|
||||
SysConfig *types.SystemConfig // system config cache
|
||||
Redis *redis.Client
|
||||
}
|
||||
|
||||
func NewServer(appConfig *types.AppConfig) *AppServer {
|
||||
func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types.SystemConfig) *AppServer {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
gin.DefaultWriter = io.Discard
|
||||
return &AppServer{
|
||||
Config: appConfig,
|
||||
Engine: gin.Default(),
|
||||
Config: appConfig,
|
||||
Redis: redis,
|
||||
Engine: gin.Default(),
|
||||
SysConfig: sysConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
s.Engine.Use(corsMiddleware())
|
||||
s.Engine.Use(staticResourceMiddleware())
|
||||
s.Engine.Use(authorizeMiddleware(s, client))
|
||||
s.Engine.Use(parameterHandlerMiddleware())
|
||||
func (s *AppServer) Init(client *redis.Client) {
|
||||
s.Engine.Use(middleware.ThumbMiddleware())
|
||||
s.Engine.Use(middleware.ParameterHandlerMiddleware())
|
||||
s.Engine.Use(errorHandler)
|
||||
// 添加静态资源访问
|
||||
s.Engine.Static("/static", s.Config.StaticDir)
|
||||
@@ -62,78 +54,26 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
// load system configs
|
||||
var sysConfig model.Config
|
||||
err := db.Where("marker", "system").First(&sysConfig).Error
|
||||
err := db.Where("name", "system").First(&sysConfig).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load system config: %v", err)
|
||||
}
|
||||
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &s.SysConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode system config: %v", err)
|
||||
}
|
||||
|
||||
// 迁移数据表
|
||||
logger.Info("Migrating database tables...")
|
||||
db.AutoMigrate(
|
||||
&model.ChatItem{},
|
||||
&model.ChatMessage{},
|
||||
&model.ChatRole{},
|
||||
&model.ChatModel{},
|
||||
&model.InviteCode{},
|
||||
&model.InviteLog{},
|
||||
&model.Menu{},
|
||||
&model.Order{},
|
||||
&model.Product{},
|
||||
&model.User{},
|
||||
&model.Function{},
|
||||
&model.File{},
|
||||
&model.Redeem{},
|
||||
&model.Config{},
|
||||
&model.ApiKey{},
|
||||
&model.AdminUser{},
|
||||
&model.AppType{},
|
||||
&model.SdJob{},
|
||||
&model.SunoJob{},
|
||||
&model.PowerLog{},
|
||||
&model.VideoJob{},
|
||||
&model.MidJourneyJob{},
|
||||
&model.UserLoginLog{},
|
||||
&model.DallJob{},
|
||||
)
|
||||
// 手动删除字段
|
||||
if db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.Order{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.User{}, "chat_config") {
|
||||
db.Migrator().DropColumn(&model.User{}, "chat_config")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatModel{}, "category") {
|
||||
db.Migrator().DropColumn(&model.ChatModel{}, "category")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatModel{}, "description") {
|
||||
db.Migrator().DropColumn(&model.ChatModel{}, "description")
|
||||
}
|
||||
|
||||
logger.Info("Database tables migrated successfully")
|
||||
|
||||
// 统计安装信息
|
||||
go func() {
|
||||
info, err := host.Info()
|
||||
if err == nil {
|
||||
apiURL := fmt.Sprintf("%s/%s", s.Config.ApiConfig.ApiURL, "api/installs/push")
|
||||
apiURL := fmt.Sprintf("%s/api/installs/push", types.GeekAPIURL)
|
||||
timestamp := time.Now().Unix()
|
||||
product := "geekai-plus"
|
||||
signStr := fmt.Sprintf("%s#%s#%d", product, info.HostID, timestamp)
|
||||
sign := utils.Sha256(signStr)
|
||||
resp, err := req.C().R().SetBody(map[string]interface{}{"product": product, "device_id": info.HostID, "timestamp": timestamp, "sign": sign}).Post(apiURL)
|
||||
if err != nil {
|
||||
logger.Errorf("register install info failed: %v", err)
|
||||
} else {
|
||||
if err == nil {
|
||||
logger.Debugf("register install info success: %v", resp.String())
|
||||
}
|
||||
}
|
||||
@@ -155,292 +95,3 @@ func errorHandler(c *gin.Context) {
|
||||
//加载完 defer recover,继续后续接口调用
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// 跨域中间件设置
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
|
||||
// 设置允许的请求源
|
||||
if origin != "" {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
//允许跨域设置可以返回其他子段,可以自定义字段
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
|
||||
// 允许浏览器(客户端)可以解析的头部 (重要)
|
||||
c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||
//设置缓存时间
|
||||
c.Header("Access-Control-Max-Age", "172800")
|
||||
//允许客户端传递校验信息比如 cookie (重要)
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if method == http.MethodOptions {
|
||||
c.JSON(http.StatusOK, "ok!")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Info("Panic info is: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 用户授权验证
|
||||
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
var tokenString string
|
||||
isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
|
||||
if isAdminApi { // 后台管理 API
|
||||
tokenString = c.GetHeader(types.AdminAuthHeader)
|
||||
} else if clientProtocols != "" { // Websocket 连接
|
||||
// 解析子协议内容
|
||||
protocols := strings.Split(clientProtocols, ",")
|
||||
if protocols[0] == "realtime" {
|
||||
tokenString = strings.TrimSpace(protocols[1][25:])
|
||||
} else if protocols[0] == "token" {
|
||||
tokenString = strings.TrimSpace(protocols[1])
|
||||
}
|
||||
} else {
|
||||
tokenString = c.GetHeader(types.UserAuthHeader)
|
||||
}
|
||||
|
||||
if tokenString == "" {
|
||||
if needLogin(c) {
|
||||
resp.NotAuth(c, "You should put Authorization in request headers")
|
||||
c.Abort()
|
||||
return
|
||||
} else { // 直接放行
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok && needLogin(c) {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
if isAdminApi {
|
||||
return []byte(s.Config.AdminSession.SecretKey), nil
|
||||
} else {
|
||||
return []byte(s.Config.Session.SecretKey), nil
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if err != nil && needLogin(c) {
|
||||
resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is invalid")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is expired")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("users/%v", claims["user_id"])
|
||||
if isAdminApi {
|
||||
key = fmt.Sprintf("admin/%v", claims["user_id"])
|
||||
}
|
||||
if _, err := client.Get(context.Background(), key).Result(); err != nil && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is not found in redis")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(types.LoginUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func needLogin(c *gin.Context) bool {
|
||||
if c.Request.URL.Path == "/api/user/login" ||
|
||||
c.Request.URL.Path == "/api/user/logout" ||
|
||||
c.Request.URL.Path == "/api/user/resetPass" ||
|
||||
c.Request.URL.Path == "/api/admin/login" ||
|
||||
c.Request.URL.Path == "/api/admin/logout" ||
|
||||
c.Request.URL.Path == "/api/admin/login/captcha" ||
|
||||
c.Request.URL.Path == "/api/user/register" ||
|
||||
c.Request.URL.Path == "/api/chat/history" ||
|
||||
c.Request.URL.Path == "/api/chat/detail" ||
|
||||
c.Request.URL.Path == "/api/chat/list" ||
|
||||
c.Request.URL.Path == "/api/app/list" ||
|
||||
c.Request.URL.Path == "/api/app/type/list" ||
|
||||
c.Request.URL.Path == "/api/app/list/user" ||
|
||||
c.Request.URL.Path == "/api/model/list" ||
|
||||
c.Request.URL.Path == "/api/mj/imgWall" ||
|
||||
c.Request.URL.Path == "/api/mj/notify" ||
|
||||
c.Request.URL.Path == "/api/invite/hits" ||
|
||||
c.Request.URL.Path == "/api/sd/imgWall" ||
|
||||
c.Request.URL.Path == "/api/dall/imgWall" ||
|
||||
c.Request.URL.Path == "/api/product/list" ||
|
||||
c.Request.URL.Path == "/api/menu/list" ||
|
||||
c.Request.URL.Path == "/api/markMap/client" ||
|
||||
c.Request.URL.Path == "/api/payment/doPay" ||
|
||||
c.Request.URL.Path == "/api/payment/payWays" ||
|
||||
c.Request.URL.Path == "/api/suno/detail" ||
|
||||
c.Request.URL.Path == "/api/suno/play" ||
|
||||
c.Request.URL.Path == "/api/download" ||
|
||||
c.Request.URL.Path == "/api/dall/models" ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/payment/notify/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 统一参数处理
|
||||
func parameterHandlerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// GET 参数处理
|
||||
params := c.Request.URL.Query()
|
||||
for key, values := range params {
|
||||
for i, value := range values {
|
||||
params[key][i] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
// update get parameters
|
||||
c.Request.URL.RawQuery = params.Encode()
|
||||
// skip file upload requests
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
// process POST JSON request body
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 还原请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
// 将请求体解析为 JSON
|
||||
var jsonData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&jsonData); err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 对 JSON 数据中的字符串值去除两端空格
|
||||
trimJSONStrings(jsonData)
|
||||
// 更新请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 递归对 JSON 数据中的字符串值去除两端空格
|
||||
func trimJSONStrings(data interface{}) {
|
||||
switch v := data.(type) {
|
||||
case map[string]interface{}:
|
||||
for key, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[key] = strings.TrimSpace(valueType)
|
||||
case map[string]interface{}, []interface{}:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for i, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[i] = strings.TrimSpace(valueType)
|
||||
case map[string]interface{}, []interface{}:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 静态资源中间件
|
||||
func staticResourceMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
url := c.Request.URL.String()
|
||||
// 拦截生成缩略图请求
|
||||
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
|
||||
r := strings.SplitAfter(url, "imageView2")
|
||||
size := strings.Split(r[1], "/")
|
||||
if len(size) != 8 {
|
||||
c.String(http.StatusNotFound, "invalid thumb args")
|
||||
return
|
||||
}
|
||||
with := utils.IntValue(size[3], 0)
|
||||
height := utils.IntValue(size[5], 0)
|
||||
quality := utils.IntValue(size[7], 75)
|
||||
|
||||
// 打开图片文件
|
||||
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "Image not found")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(file)
|
||||
// for .webp image
|
||||
if err != nil {
|
||||
img, err = webp.Decode(file)
|
||||
}
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Error decoding image")
|
||||
return
|
||||
}
|
||||
|
||||
var newImg image.Image
|
||||
if height == 0 || with == 0 {
|
||||
// 固定宽度,高度自适应
|
||||
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
|
||||
} else {
|
||||
// 生成缩略图
|
||||
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 设置图片缓存有效期为一年 (365天)
|
||||
c.Header("Cache-Control", "max-age=31536000, public")
|
||||
// 直接输出图像数据流
|
||||
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
||||
c.Abort() // 中断请求
|
||||
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
+109
-4
@@ -10,14 +10,16 @@ package core
|
||||
import (
|
||||
"bytes"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/log"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
var logger = log.GetLogger()
|
||||
|
||||
func NewDefaultConfig() *types.AppConfig {
|
||||
return &types.AppConfig{
|
||||
@@ -30,7 +32,6 @@ func NewDefaultConfig() *types.AppConfig {
|
||||
SecretKey: utils.RandString(64),
|
||||
MaxAge: 86400,
|
||||
},
|
||||
ApiConfig: types.ApiConfig{},
|
||||
OSS: types.OSSConfig{
|
||||
Active: "local",
|
||||
Local: types.LocalStorageConfig{
|
||||
@@ -38,7 +39,6 @@ func NewDefaultConfig() *types.AppConfig {
|
||||
BasePath: "./static/upload",
|
||||
},
|
||||
},
|
||||
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,3 +74,108 @@ func SaveConfig(config *types.AppConfig) error {
|
||||
|
||||
return os.WriteFile(config.Path, buf.Bytes(), 0644)
|
||||
}
|
||||
|
||||
func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
|
||||
// 加载系统配置
|
||||
var sysConfig model.Config
|
||||
var baseConfig types.BaseConfig
|
||||
db.Where("name", "system").First(&sysConfig)
|
||||
err := utils.JsonDecode(sysConfig.Value, &baseConfig)
|
||||
if err != nil {
|
||||
logger.Error("load system config error: ", err)
|
||||
}
|
||||
|
||||
// 加载验证码配置
|
||||
var captchaConfig types.CaptchaConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyCaptcha).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &captchaConfig)
|
||||
if err != nil {
|
||||
logger.Error("load geek service config error: ", err)
|
||||
}
|
||||
|
||||
// 加载微信登录配置
|
||||
var wxLoginConfig types.WxLoginConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyWxLogin).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &wxLoginConfig)
|
||||
if err != nil {
|
||||
logger.Error("load wx login config error: ", err)
|
||||
}
|
||||
|
||||
// 加载短信配置
|
||||
var smsConfig types.SMSConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeySms).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &smsConfig)
|
||||
if err != nil {
|
||||
logger.Error("load sms config error: ", err)
|
||||
}
|
||||
|
||||
// 加载 OSS 配置
|
||||
var ossConfig types.OSSConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyOss).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &ossConfig)
|
||||
if err != nil {
|
||||
logger.Error("load oss config error: ", err)
|
||||
}
|
||||
|
||||
// 加载 SMTP 配置
|
||||
var smtpConfig types.SmtpConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeySmtp).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &smtpConfig)
|
||||
if err != nil {
|
||||
logger.Error("load smtp config error: ", err)
|
||||
}
|
||||
|
||||
// 加载支付配置
|
||||
var paymentConfig types.PaymentConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyPayment).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &paymentConfig)
|
||||
if err != nil {
|
||||
logger.Error("load payment config error: ", err)
|
||||
}
|
||||
|
||||
// 加载文本审查配置
|
||||
var moderationConfig types.ModerationConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyModeration).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &moderationConfig)
|
||||
if err != nil {
|
||||
logger.Error("load moderation config error: ", err)
|
||||
}
|
||||
|
||||
// 加载即梦AI配置
|
||||
var jimengConfig types.JimengConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyJimeng).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &jimengConfig)
|
||||
if err != nil {
|
||||
logger.Error("load jimeng config error: ", err)
|
||||
}
|
||||
|
||||
// 加载微信公众号配置
|
||||
var wxGzhConfig types.WxGzhConfig
|
||||
sysConfig.Id = 0
|
||||
db.Where("name", types.ConfigKeyWxGzh).First(&sysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &wxGzhConfig)
|
||||
if err != nil {
|
||||
logger.Error("load wx gzh config error: ", err)
|
||||
}
|
||||
|
||||
return &types.SystemConfig{
|
||||
Base: baseConfig,
|
||||
SMS: smsConfig,
|
||||
OSS: ossConfig,
|
||||
SMTP: smtpConfig,
|
||||
Payment: paymentConfig,
|
||||
Captcha: captchaConfig,
|
||||
WxLogin: wxLoginConfig,
|
||||
Moderation: moderationConfig,
|
||||
Jimeng: jimengConfig,
|
||||
WxGzh: wxGzhConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/log"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var logger = log.GetLogger()
|
||||
|
||||
// 前端用户授权验证
|
||||
func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tokenString := c.GetHeader(types.UserAuthHeader)
|
||||
if tokenString == "" {
|
||||
resp.NotAuth(c, "无效的授权令牌")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("不支持的令牌签名方法: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(secretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
resp.NotAuth(c, fmt.Sprintf("解析授权令牌失败: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
resp.NotAuth(c, "令牌无效")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() {
|
||||
resp.NotAuth(c, "令牌过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("users/%v", claims["user_id"])
|
||||
if _, err := redis.Get(context.Background(), key).Result(); err != nil {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(types.LoginUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// 管理后台用户授权验证
|
||||
func AdminAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tokenString := c.GetHeader(types.AdminAuthHeader)
|
||||
if tokenString == "" {
|
||||
resp.NotAuth(c, "无效的授权令牌")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("不支持的令牌签名方法: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(secretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
resp.NotAuth(c, fmt.Sprintf("解析授权令牌失败: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
resp.NotAuth(c, "令牌无效")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() {
|
||||
resp.NotAuth(c, "令牌过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("admin/%v", claims["user_id"])
|
||||
if _, err := redis.Get(context.Background(), key).Result(); err != nil {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(types.AdminUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 统一参数处理
|
||||
func ParameterHandlerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// GET 参数处理
|
||||
params := c.Request.URL.Query()
|
||||
for key, values := range params {
|
||||
for i, value := range values {
|
||||
params[key][i] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
// update get parameters
|
||||
c.Request.URL.RawQuery = params.Encode()
|
||||
// skip file upload requests
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
// process POST JSON request body
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 还原请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
// 将请求体解析为 JSON
|
||||
var jsonData map[string]any
|
||||
if err := c.ShouldBindJSON(&jsonData); err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 对 JSON 数据中的字符串值去除两端空格
|
||||
trimJSONStrings(jsonData)
|
||||
// 更新请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 递归对 JSON 数据中的字符串值去除两端空格
|
||||
func trimJSONStrings(data any) {
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
for key, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[key] = strings.TrimSpace(valueType)
|
||||
case map[string]any, []any:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for i, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[i] = strings.TrimSpace(valueType)
|
||||
case map[string]any, []any:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
// RateLimitEvery 使用 Redis 做固定间隔限流:在 interval 内仅允许一次请求
|
||||
// Key 优先使用登录用户ID,若没有则退化为 route + IP
|
||||
func RateLimitEvery(redisClient *redis.Client, interval time.Duration) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
keyID := ""
|
||||
if userID, ok := c.Get(types.LoginUserID); ok {
|
||||
keyID = fmt.Sprintf("user:%s", utils.InterfaceToString(userID))
|
||||
} else {
|
||||
keyID = fmt.Sprintf("ip:%s", c.ClientIP())
|
||||
}
|
||||
|
||||
fullPath := c.FullPath()
|
||||
if fullPath == "" {
|
||||
fullPath = c.Request.URL.Path
|
||||
}
|
||||
key := fmt.Sprintf("rl:%s:%s", fullPath, keyID)
|
||||
|
||||
okSet, err := redisClient.SetNX(context.Background(), key, 1, interval).Result()
|
||||
if err != nil {
|
||||
// Redis 异常时放行,避免误伤可用性
|
||||
return
|
||||
}
|
||||
if !okSet {
|
||||
c.JSON(http.StatusTooManyRequests, types.BizVo{Code: types.Failed, Message: "请求过于频繁,请稍后重试"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"geekai/utils"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nfnt/resize"
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// 缩略图中间件
|
||||
func ThumbMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
url := c.Request.URL.String()
|
||||
// 拦截生成缩略图请求
|
||||
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
|
||||
r := strings.SplitAfter(url, "imageView2")
|
||||
size := strings.Split(r[1], "/")
|
||||
if len(size) != 8 {
|
||||
c.String(http.StatusNotFound, "invalid thumb args")
|
||||
return
|
||||
}
|
||||
with := utils.IntValue(size[3], 0)
|
||||
height := utils.IntValue(size[5], 0)
|
||||
quality := utils.IntValue(size[7], 75)
|
||||
|
||||
// 打开图片文件
|
||||
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "Image not found")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(file)
|
||||
// for .webp image
|
||||
if err != nil {
|
||||
img, err = webp.Decode(file)
|
||||
}
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Error decoding image")
|
||||
return
|
||||
}
|
||||
|
||||
var newImg image.Image
|
||||
if height == 0 || with == 0 {
|
||||
// 固定宽度,高度自适应
|
||||
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
|
||||
} else {
|
||||
// 生成缩略图
|
||||
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 设置图片缓存有效期为一年 (365天)
|
||||
c.Header("Cache-Control", "max-age=31536000, public")
|
||||
// 直接输出图像数据流
|
||||
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
||||
c.Abort() // 中断请求
|
||||
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
+68
-122
@@ -17,87 +17,17 @@ type AppConfig struct {
|
||||
Session Session
|
||||
AdminSession Session
|
||||
ProxyURL string
|
||||
MysqlDns string // mysql 连接地址
|
||||
StaticDir string // 静态资源目录
|
||||
StaticUrl string // 静态资源 URL
|
||||
Redis RedisConfig // redis 连接信息
|
||||
ApiConfig ApiConfig // ChatPlus API authorization configs
|
||||
SMS SMSConfig // send mobile message config
|
||||
OSS OSSConfig // OSS config
|
||||
SmtpConfig SmtpConfig // 邮件发送配置
|
||||
XXLConfig XXLConfig
|
||||
AlipayConfig AlipayConfig // 支付宝支付渠道配置
|
||||
HuPiPayConfig HuPiPayConfig // 虎皮椒支付配置
|
||||
GeekPayConfig GeekPayConfig // GEEK 支付配置
|
||||
WechatPayConfig WechatPayConfig // 微信支付渠道配置
|
||||
TikaHost string // TiKa 服务器地址
|
||||
}
|
||||
|
||||
type SmtpConfig struct {
|
||||
UseTls bool // 是否使用 TLS 发送
|
||||
Host string
|
||||
Port int
|
||||
AppName string // 应用名称
|
||||
From string // 发件人邮箱地址
|
||||
Password string // 发件人邮箱密码
|
||||
}
|
||||
|
||||
type ApiConfig struct {
|
||||
ApiURL string
|
||||
AppId string
|
||||
Token string
|
||||
}
|
||||
|
||||
type AlipayConfig struct {
|
||||
Enabled bool // 是否启用该支付通道
|
||||
SandBox bool // 是否沙盒环境
|
||||
AppId string // 应用 ID
|
||||
UserId string // 支付宝用户 ID
|
||||
PrivateKey string // 用户私钥文件路径
|
||||
PublicKey string // 用户公钥文件路径
|
||||
AlipayPublicKey string // 支付宝公钥文件路径
|
||||
RootCert string // Root 秘钥路径
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
}
|
||||
|
||||
type WechatPayConfig struct {
|
||||
Enabled bool // 是否启用该支付通道
|
||||
AppId string // 公众号的APPID,如:wxd678efh567hg6787
|
||||
MchId string // 直连商户的商户号,由微信支付生成并下发
|
||||
SerialNo string // 商户证书的证书序列号
|
||||
PrivateKey string // 用户私钥文件路径
|
||||
ApiV3Key string // API V3 秘钥
|
||||
NotifyURL string // 异步通知地址
|
||||
}
|
||||
|
||||
type HuPiPayConfig struct { //虎皮椒第四方支付配置
|
||||
Enabled bool // 是否启用该支付通道
|
||||
AppId string // App ID
|
||||
AppSecret string // app 密钥
|
||||
ApiURL string // 支付网关
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
}
|
||||
|
||||
// GeekPayConfig GEEK支付配置
|
||||
type GeekPayConfig struct {
|
||||
Enabled bool
|
||||
AppId string // 商户 ID
|
||||
PrivateKey string // 私钥
|
||||
ApiURL string // API 网关
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
Methods []string // 支付方式
|
||||
}
|
||||
|
||||
type XXLConfig struct { // XXL 任务调度配置
|
||||
Enabled bool
|
||||
ServerAddr string
|
||||
ExecutorIp string
|
||||
ExecutorPort string
|
||||
AccessToken string
|
||||
RegistryKey string
|
||||
MysqlDns string // mysql 连接地址
|
||||
StaticDir string // 静态资源目录
|
||||
StaticUrl string // 静态资源 URL
|
||||
Redis RedisConfig // redis 连接信息
|
||||
SMS SMSConfig // send mobile message config
|
||||
OSS OSSConfig // OSS config
|
||||
SmtpConfig SmtpConfig // 邮件发送配置
|
||||
AlipayConfig AlipayConfig // 支付宝支付渠道配置
|
||||
GeekPayConfig EpayConfig // GEEK 支付配置
|
||||
WechatPayConfig WxPayConfig // 微信支付渠道配置
|
||||
TikaHost string // TiKa 服务器地址
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
@@ -107,70 +37,86 @@ type RedisConfig struct {
|
||||
DB int
|
||||
}
|
||||
|
||||
// LicenseKey 存储许可证书的 KEY
|
||||
const LicenseKey = "Geek-AI-License"
|
||||
|
||||
type License struct {
|
||||
Key string `json:"key"` // 许可证书密钥
|
||||
MachineId string `json:"machine_id"` // 机器码
|
||||
ExpiredAt int64 `json:"expired_at"` // 过期时间
|
||||
IsActive bool `json:"is_active"` // 是否激活
|
||||
Configs LicenseConfig `json:"configs"`
|
||||
}
|
||||
|
||||
type LicenseConfig struct {
|
||||
UserNum int `json:"user_num"` // 用户数量
|
||||
DeCopy bool `json:"de_copy"` // 去版权
|
||||
}
|
||||
|
||||
func (c RedisConfig) Url() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
Title string `json:"title,omitempty"` // 网站标题
|
||||
Slogan string `json:"slogan,omitempty"` // 网站 slogan
|
||||
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
|
||||
Logo string `json:"logo,omitempty"` // 圆形 Logo
|
||||
BarLogo string `json:"bar_logo,omitempty"` // 条形 Logo
|
||||
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
VipMonthPower int `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值
|
||||
type BaseConfig struct {
|
||||
Title string `json:"title,omitempty"` // 网站标题
|
||||
Slogan string `json:"slogan,omitempty"` // 网站 slogan
|
||||
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
|
||||
Logo string `json:"logo,omitempty"` // 圆形 Logo
|
||||
BarLogo string `json:"bar_logo,omitempty"` // 条形 Logo
|
||||
|
||||
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册
|
||||
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册
|
||||
|
||||
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
|
||||
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
|
||||
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间,单位:分钟
|
||||
|
||||
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||
DallPower int `json:"dall_power,omitempty"` // DALL-E-3 绘图消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力,未配置分项时回退用
|
||||
MjUpscalePower int `json:"mj_upscale_power,omitempty"` // MJ 放大/变换消耗算力
|
||||
MjBlendPower int `json:"mj_blend_power,omitempty"` // MJ 融图消耗算力
|
||||
MjSwapFacePower int `json:"mj_swap_face_power,omitempty"` // MJ 换脸消耗算力
|
||||
MjModalPower int `json:"mj_modal_power,omitempty"` // MJ 局部重绘消耗算力
|
||||
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
|
||||
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力
|
||||
AdvanceVoicePower int `json:"advance_voice_power,omitempty"` // 高级语音对话消耗算力
|
||||
PromptPower int `json:"prompt_power,omitempty"` // 生成提示词消耗算力
|
||||
|
||||
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
|
||||
|
||||
EnableContext bool `json:"enable_context,omitempty"`
|
||||
ContextDeep int `json:"context_deep,omitempty"`
|
||||
|
||||
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
|
||||
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
||||
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
||||
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
DefaultNickname string `json:"default_nickname"` // 默认昵称
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
IndexPage string `json:"index_page"` // 首页显示的页面
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
GaBeian string `json:"ga_beian"` // 公安备案号
|
||||
|
||||
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
TranslateModelId int `json:"translate_model_id"` // 用来做提示词翻译的大模型 id
|
||||
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||
|
||||
EnableMobileSite bool `json:"enable_mobile_site"` // 是否开启手机站点
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
Base BaseConfig
|
||||
Payment PaymentConfig
|
||||
OSS OSSConfig
|
||||
SMS SMSConfig
|
||||
SMTP SmtpConfig
|
||||
Captcha CaptchaConfig
|
||||
WxLogin WxLoginConfig
|
||||
Jimeng JimengConfig
|
||||
Moderation ModerationConfig
|
||||
WxGzh WxGzhConfig
|
||||
}
|
||||
|
||||
// 配置键名常量
|
||||
const (
|
||||
ConfigKeySystem = "system" // 系统配置
|
||||
ConfigKeyNotice = "notice" // 公告配置
|
||||
ConfigKeyAgreement = "agreement" // 用户协议配置
|
||||
ConfigKeyPrivacy = "privacy" // 隐私政策配置
|
||||
ConfigKeyMarkMap = "mark_map" // 水印配置
|
||||
ConfigKeyCaptcha = "captcha" // 验证码配置
|
||||
ConfigKeyWxLogin = "wx_login" // 微信扫码登录配置
|
||||
ConfigKeyWxGzh = "wx_gzh" // 微信公众号配置
|
||||
ConfigKeySms = "sms" // 短信配置
|
||||
ConfigKeySmtp = "smtp" // SMTP 配置
|
||||
ConfigKeyOss = "oss" // OSS 配置
|
||||
ConfigKeyPayment = "payment" // 支付配置
|
||||
ConfigKeyModeration = "moderation" // 文本审查配置
|
||||
ConfigKeyAI3D = "ai3d" // AI3D 配置
|
||||
ConfigKeyJimeng = "jimeng" // 即梦AI配置
|
||||
ConfigKeyVideo = "video" // 视频生成配置
|
||||
ConfigKeyPPT = "ppt" // PPT 生成配置
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package types
|
||||
|
||||
import "os"
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// GeekAI 增值服务
|
||||
var GeekAPIURL = "https://sapi.geekai.me"
|
||||
|
||||
func init() {
|
||||
if os.Getenv("GEEK_API_URL") != "" {
|
||||
GeekAPIURL = os.Getenv("GEEK_API_URL")
|
||||
}
|
||||
}
|
||||
|
||||
// CaptchaConfig 行为验证码配置
|
||||
type CaptchaConfig struct {
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
Type string `json:"type,omitempty"` // 验证码类型, 可选值: "dot" 或 "slide"
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
// WxLoginConfig 微信扫码登录配置
|
||||
type WxLoginConfig struct {
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
NotifyURL string `json:"notify_url,omitempty"` // 登录成功回调 URL
|
||||
Enabled bool `json:"enabled,omitempty"` // 是否启用微信登录
|
||||
}
|
||||
|
||||
// 微信公众号配置
|
||||
type WxGzhConfig struct {
|
||||
AppId string `json:"app_id,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Token string `json:"token"`
|
||||
EncodingAESKey string `json:"encoding_aes_key"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package types
|
||||
|
||||
// JimengConfig 即梦AI配置
|
||||
type JimengConfig struct {
|
||||
// 即梦AI的AccessKey和SecretKey
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
SecretKey string `json:"secret_key,omitempty"`
|
||||
// 火山引擎大模型专用的验证方式
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
// 算力配置
|
||||
Powers map[string]int `json:"powers,omitempty"`
|
||||
}
|
||||
|
||||
// JMTaskStatus 任务状态
|
||||
type JMTaskStatus string
|
||||
|
||||
const (
|
||||
JMTaskStatusSubmited = JMTaskStatus("submited") // 任务已提交
|
||||
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务排队中
|
||||
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
|
||||
JMTaskStatusDone = JMTaskStatus("done") // 处理完成
|
||||
JMTaskStatusNotFound = JMTaskStatus("not_found") // 任务未找到
|
||||
JMTaskStatusSuccess = JMTaskStatus("success") // 任务成功
|
||||
JMTaskStatusFailed = JMTaskStatus("failed") // 任务失败
|
||||
JMTaskStatusExpired = JMTaskStatus("expired") // 任务过期
|
||||
)
|
||||
|
||||
// JMTaskType 任务类型
|
||||
type JMTaskType string
|
||||
|
||||
const (
|
||||
JMTaskTypeImage = JMTaskType("image") // 文生图
|
||||
JMTaskTypeVideo = JMTaskType("video") // 图生图
|
||||
JMTaskTypeVirtualHuman = JMTaskType("virtual_human") // 图像编辑
|
||||
JMTaskTypeActionTransfer = JMTaskType("action_transfer") // 图像特效
|
||||
)
|
||||
|
||||
// JimengTaskRequest 即梦AI任务请求
|
||||
type JimengTaskRequest struct {
|
||||
TaskType JMTaskType `json:"type"` // 任务类型
|
||||
ReqKey string `json:"req_key"` // 请求Key
|
||||
Action string `json:"action"` // 请求Action
|
||||
Power int `json:"power"` // 消耗算力
|
||||
// 公共参数
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
ImageUrls []string `json:"image_urls,omitempty"`
|
||||
|
||||
// 图片生成参数
|
||||
Size string `json:"size,omitempty"`
|
||||
UsePreLLM bool `json:"use_pre_llm,omitempty"`
|
||||
Scale float64 `json:"scale,omitempty"`
|
||||
ForceSingle bool `json:"force_single,omitempty"`
|
||||
|
||||
// 视频生成参数
|
||||
Duration int `json:"duration,omitempty"` // 视频时长,单位:秒
|
||||
TemplateId string `json:"template_id,omitempty"` // 运镜模板ID
|
||||
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||
CameraStrength string `json:"camera_strength,omitempty"` // 运镜强度
|
||||
|
||||
// 数字人视频生成参数
|
||||
AudioURL string `json:"audio_url,omitempty"` // 音频URL
|
||||
RecognizeKey string `json:"recognize_key,omitempty"` // 识别主体请求Key
|
||||
|
||||
// 视频动作迁移参数
|
||||
VideoURL string `json:"video_url,omitempty"` // 动作视频URL
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// 文本审查
|
||||
type ModerationConfig struct {
|
||||
Enable bool `json:"enable,omitempty"` // 是否启用文本审查
|
||||
Active string `json:"active,omitempty"`
|
||||
EnableGuide bool `json:"enable_guide,omitempty"` // 是否启用模型引导提示词
|
||||
GuidePrompt string `json:"guide_prompt,omitempty"` // 模型引导提示词
|
||||
Gitee ModerationGiteeConfig `json:"gitee,omitempty"`
|
||||
Baidu ModerationBaiduConfig `json:"baidu,omitempty"`
|
||||
Tencent ModerationTencentConfig `json:"tencent,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
ModerationGitee = "gitee"
|
||||
ModerationBaidu = "baidu"
|
||||
ModerationTencent = "tencent"
|
||||
)
|
||||
|
||||
// GiteeAI 文本审查配置
|
||||
type ModerationGiteeConfig struct {
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
Model string `json:"model,omitempty"` // 文本审核模型
|
||||
}
|
||||
|
||||
// 百度文本审查配置
|
||||
type ModerationBaiduConfig struct {
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
SecretKey string `json:"secret_key,omitempty"`
|
||||
}
|
||||
|
||||
// 腾讯云文本审查配置
|
||||
type ModerationTencentConfig struct {
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
SecretKey string `json:"secret_key,omitempty"`
|
||||
}
|
||||
|
||||
type ModerationResult struct {
|
||||
Flagged bool `json:"flagged,omitempty"`
|
||||
Categories map[string]bool `json:"categories,omitempty"`
|
||||
CategoryScores map[string]float64 `json:"category_scores,omitempty"`
|
||||
}
|
||||
|
||||
var ModerationCategories = map[string]string{
|
||||
"politic": "内容涉及人物、事件或敏感的政治观点",
|
||||
"porn": "明确的色情内容",
|
||||
"insult": "具有侮辱、攻击性语言、人身攻击或冒犯性表达",
|
||||
"violence": "包含暴力、血腥、攻击行为或煽动暴力的言论",
|
||||
"illegal": "涉及违法活动的内容,如诈骗、赌博等",
|
||||
"terror": "宣扬恐怖主义、极端暴力或煽动恐怖行为的内容",
|
||||
"ad": "垃圾广告或未经许可的推广内容",
|
||||
"spam": "无意义重复内容或诱导性信息",
|
||||
"abuse": "人身攻击、恶意辱骂或侮辱性言论",
|
||||
"polity": "涉及国家政治、领导人或政策的违规讨论内容",
|
||||
}
|
||||
|
||||
// 敏感词来源
|
||||
const (
|
||||
ModerationSourceChat = "chat"
|
||||
ModerationSourceMJ = "mj"
|
||||
ModerationSourceImage = "image"
|
||||
ModerationSourceSuno = "suno"
|
||||
ModerationSourceVideo = "video"
|
||||
ModerationSourceJiMeng = "jimeng"
|
||||
)
|
||||
+12
-16
@@ -11,29 +11,25 @@ type OrderStatus int
|
||||
|
||||
const (
|
||||
OrderNotPaid = OrderStatus(0)
|
||||
OrderScanned = OrderStatus(1) // 已扫码
|
||||
OrderPaidSuccess = OrderStatus(2)
|
||||
OrderPaidSuccess = OrderStatus(2) // 已支付
|
||||
OrderPaidFailed = OrderStatus(3) // 已关闭
|
||||
)
|
||||
|
||||
type OrderRemark struct {
|
||||
Days int `json:"days"` // 有效期
|
||||
Power int `json:"power"` // 增加算力点数
|
||||
Name string `json:"name"` // 产品名称
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
Days int `json:"days"` // 有效期
|
||||
Power int `json:"power"` // 增加算力点数
|
||||
Name string `json:"name"` // 产品名称
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
var PayMethods = map[string]string{
|
||||
// PayChannel 支付渠道
|
||||
var PayChannel = map[string]string{
|
||||
"alipay": "支付宝商号",
|
||||
"wechat": "微信商号",
|
||||
"hupi": "虎皮椒",
|
||||
"geek": "易支付",
|
||||
"wxpay": "微信商号",
|
||||
"epay": "易支付",
|
||||
}
|
||||
var PayNames = map[string]string{
|
||||
|
||||
var PayWays = map[string]string{
|
||||
"alipay": "支付宝",
|
||||
"wxpay": "微信支付",
|
||||
"qqpay": "QQ钱包",
|
||||
"jdpay": "京东支付",
|
||||
"douyin": "抖音支付",
|
||||
"paypal": "PayPal支付",
|
||||
}
|
||||
|
||||
+38
-26
@@ -8,41 +8,53 @@ package types
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type OSSConfig struct {
|
||||
Active string
|
||||
Local LocalStorageConfig
|
||||
Minio MiniOssConfig
|
||||
QiNiu QiNiuOssConfig
|
||||
AliYun AliYunOssConfig
|
||||
Active string `json:"active,omitempty"`
|
||||
Local LocalStorageConfig `json:"local,omitempty"`
|
||||
Minio MiniOssConfig `json:"minio,omitempty"`
|
||||
QiNiu QiNiuOssConfig `json:"qiniu,omitempty"`
|
||||
AliYun AliYunOssConfig `json:"aliyun,omitempty"`
|
||||
Tencent TencentOssConfig `json:"tencent,omitempty"`
|
||||
}
|
||||
|
||||
type MiniOssConfig struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
UseSSL bool
|
||||
Domain string
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
UseSSL bool `json:"use_ssl,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符
|
||||
}
|
||||
|
||||
type QiNiuOssConfig struct {
|
||||
Zone string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
Domain string
|
||||
Zone string `json:"zone,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/4/w/{width}/h/{height}/q/75
|
||||
}
|
||||
|
||||
type AliYunOssConfig struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
Domain string
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?x-oss-process=image/resize,m_lfit,w_{width},h_{height}
|
||||
}
|
||||
|
||||
type LocalStorageConfig struct {
|
||||
BasePath string
|
||||
BaseURL string
|
||||
BasePath string `json:"base_path,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty"`
|
||||
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/4/w/{width}/h/{height}/q/75
|
||||
}
|
||||
|
||||
type TencentOssConfig struct {
|
||||
Region string `json:"region,omitempty"`
|
||||
SecretId string `json:"secret_id,omitempty"`
|
||||
SecretKey string `json:"secret_key,omitempty"`
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/1/w/{width}/h/{height}/format/jpg
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package types
|
||||
|
||||
type PaymentConfig struct {
|
||||
Alipay AlipayConfig `json:"alipay,omitempty"` // 支付宝支付渠道配置
|
||||
Epay EpayConfig `json:"epay,omitempty"` // 易支付配置
|
||||
WxPay WxPayConfig `json:"wxpay,omitempty"` // 微信支付渠道配置
|
||||
}
|
||||
|
||||
// AlipayConfig 支付宝支付配置
|
||||
type AlipayConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||
SandBox bool `json:"sandbox,omitempty"` // 是否沙盒环境
|
||||
AppId string `json:"app_id,omitempty"` // 应用 ID
|
||||
PrivateKey string `json:"private_key,omitempty"` // 应用私钥
|
||||
AlipayPublicKey string `json:"alipay_public_key,omitempty"` // 支付宝公钥
|
||||
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||
}
|
||||
|
||||
func (c *AlipayConfig) Equal(other *AlipayConfig) bool {
|
||||
return c.AppId == other.AppId &&
|
||||
c.PrivateKey == other.PrivateKey &&
|
||||
c.AlipayPublicKey == other.AlipayPublicKey &&
|
||||
c.Domain == other.Domain
|
||||
}
|
||||
|
||||
// WxPayConfig 微信支付配置
|
||||
type WxPayConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||
AppId string `json:"app_id,omitempty"` // 公众号的APPID,如:wxd678efh567hg6787
|
||||
MchId string `json:"mch_id,omitempty"` // 直连商户的商户号,由微信支付生成并下发
|
||||
SerialNo string `json:"serial_no,omitempty"` // 商户证书的证书序列号
|
||||
PrivateKey string `json:"private_key,omitempty"` // 商户证书私钥
|
||||
ApiV3Key string `json:"api_v3_key,omitempty"` // API V3 秘钥
|
||||
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||
}
|
||||
|
||||
func (c *WxPayConfig) Equal(other *WxPayConfig) bool {
|
||||
return c.AppId == other.AppId &&
|
||||
c.MchId == other.MchId &&
|
||||
c.SerialNo == other.SerialNo &&
|
||||
c.PrivateKey == other.PrivateKey &&
|
||||
c.ApiV3Key == other.ApiV3Key &&
|
||||
c.Domain == other.Domain
|
||||
}
|
||||
|
||||
// EpayConfig 易支付配置
|
||||
type EpayConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||
AppId string `json:"app_id,omitempty"` // 商户 ID
|
||||
PrivateKey string `json:"private_key,omitempty"` // 私钥
|
||||
ApiURL string `json:"api_url,omitempty"` // z支付 API 网关
|
||||
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||
}
|
||||
|
||||
func (c *EpayConfig) Equal(other *EpayConfig) bool {
|
||||
return c.AppId == other.AppId &&
|
||||
c.PrivateKey == other.PrivateKey &&
|
||||
c.ApiURL == other.ApiURL &&
|
||||
c.Domain == other.Domain
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// PPTImageProvider PPT 图片生成提供方
|
||||
type PPTImageProvider string
|
||||
|
||||
const (
|
||||
PPTImageProviderNanoBanana PPTImageProvider = "nano_banana"
|
||||
PPTImageProviderSeedream PPTImageProvider = "seedream"
|
||||
)
|
||||
|
||||
// PPTConfig PPT 生成配置(存储在 config 表,name = 'ppt')
|
||||
type PPTConfig struct {
|
||||
// 分镜 LLM 配置
|
||||
OutlineLLMApiURL string `json:"outline_llm_api_url"` // 分镜 LLM API 地址
|
||||
OutlineLLMApiKey string `json:"outline_llm_api_key"` // 分镜 LLM API Key
|
||||
OutlineLLMModel string `json:"outline_llm_model"` // 分镜 LLM 模型名称,如 gpt-4o-mini
|
||||
|
||||
// 图片生成通用配置
|
||||
ActiveImageProvider PPTImageProvider `json:"active_image_provider"` // 当前启用的图片模型提供方
|
||||
MaxSlidesPerTask int `json:"max_slides_per_task"` // 单个任务最多生成的 PPT 页数
|
||||
PowerCostPerSlide int `json:"power_cost_per_slide"` // 每张 PPT 图片消耗的算力
|
||||
MaxConcurrentRequests int `json:"max_concurrent_requests"` // 图片生成最大并发数
|
||||
QPSLimit int `json:"qps_limit"` // 外部图片 API 的 QPS 限制
|
||||
|
||||
// Nano Banana 配置
|
||||
NanoBananaApiURL string `json:"nano_banana_api_url"` // Nano Banana API 地址,如 https://xxx/v1/images/generations
|
||||
NanoBananaApiKey string `json:"nano_banana_api_key"` // Nano Banana API Key
|
||||
NanoBananaModel string `json:"nano_banana_model"` // 模型名称,如 nano-banana、nano-banana-hd
|
||||
NanoBananaResponseFormat string `json:"nano_banana_response_format"` // 响应格式:url 或 b64_json
|
||||
NanoBananaAspectRatio string `json:"nano_banana_aspect_ratio"` // 宽高比,如 1:1、4:3、16:9;空则默认 16:9
|
||||
|
||||
// Doubao Seedream 配置
|
||||
SeedreamBaseURL string `json:"seedream_base_url"` // Seedream base url,例如:https://ark.cn-beijing.volces.com/api/v3
|
||||
SeedreamApiKey string `json:"seedream_api_key"` // Seedream API Key(ARK_API_KEY)
|
||||
SeedreamModel string `json:"seedream_model"` // Seedream 模型 ID,例如:doubao-seedream-5-0-260128
|
||||
SeedreamSize string `json:"seedream_size"` // 图片尺寸 WxH(如 1920x1080)或 2K/4K 等;空则默认 1920x1080(16:9)
|
||||
SeedreamOutputFormat string `json:"seedream_output_format"` // 输出格式,例如:png
|
||||
SeedreamResponseType string `json:"seedream_response_format"` // 响应格式,例如:url
|
||||
SeedreamWatermark bool `json:"seedream_watermark"` // 是否开启水印
|
||||
}
|
||||
@@ -8,6 +8,7 @@ package types
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
const LoginUserID = "LOGIN_USER_ID"
|
||||
const AdminUserID = "ADMIN_USER_ID"
|
||||
const LoginUserCache = "LOGIN_USER_CACHE"
|
||||
|
||||
const UserAuthHeader = "Authorization"
|
||||
|
||||
+22
-14
@@ -8,26 +8,34 @@ package types
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type SMSConfig struct {
|
||||
Active string
|
||||
Ali SmsConfigAli
|
||||
Bao SmsConfigBao
|
||||
Active string `json:"active,omitempty"`
|
||||
Ali SmsConfigAli `json:"aliyun,omitempty"`
|
||||
Bao SmsConfigBao `json:"bao,omitempty"`
|
||||
Tencent SmsConfigTencent `json:"tencent,omitempty"`
|
||||
}
|
||||
|
||||
// SmsConfigAli 阿里云短信平台配置
|
||||
type SmsConfigAli struct {
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Product string
|
||||
Domain string
|
||||
Sign string // 短信签名
|
||||
CodeTempId string // 验证码短信模板 ID
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
Sign string `json:"sign,omitempty"` // 短信签名
|
||||
CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板 ID
|
||||
}
|
||||
|
||||
// SmsConfigBao 短信宝平台配置
|
||||
type SmsConfigBao struct {
|
||||
Username string //短信宝平台注册的用户名
|
||||
Password string //短信宝平台注册的密码
|
||||
Domain string //域名
|
||||
Sign string // 短信签名
|
||||
CodeTemplate string // 验证码短信模板 匹配
|
||||
Username string `json:"username,omitempty"` //短信宝平台注册的用户名
|
||||
Password string `json:"password,omitempty"` //短信宝平台注册的密码
|
||||
Sign string `json:"sign,omitempty"` // 短信签名
|
||||
CodeTemplate string `json:"code_template,omitempty"` // 验证码短信模板 匹配
|
||||
}
|
||||
|
||||
// SmsConfigTencent 腾讯云短信平台配置
|
||||
type SmsConfigTencent struct {
|
||||
SecretId string `json:"secret_id,omitempty"` // 腾讯云 SecretId
|
||||
SecretKey string `json:"secret_key,omitempty"` // 腾讯云 SecretKey
|
||||
SmsSdkAppId string `json:"sms_sdk_app_id,omitempty"` // 短信应用ID
|
||||
Sign string `json:"sign,omitempty"` // 短信签名
|
||||
CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板ID
|
||||
Region string `json:"region,omitempty"` // 地区,默认 ap-guangzhou
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type SmtpConfig struct {
|
||||
UseTls bool `json:"use_tls,omitempty"` // 是否使用 TLS 发送
|
||||
Host string `json:"host,omitempty"` // 邮件服务器地址
|
||||
Port int `json:"port,omitempty"` // 邮件服务器端口
|
||||
AppName string `json:"app_name,omitempty"` // 应用名称
|
||||
From string `json:"from,omitempty"` // 发件人邮箱地址
|
||||
Password string `json:"password,omitempty"` // 发件人邮箱密码
|
||||
}
|
||||
|
||||
func (s *SmtpConfig) Equal(other *SmtpConfig) bool {
|
||||
return s.UseTls == other.UseTls &&
|
||||
s.Host == other.Host &&
|
||||
s.Port == other.Port &&
|
||||
s.AppName == other.AppName &&
|
||||
s.From == other.From &&
|
||||
s.Password == other.Password
|
||||
}
|
||||
+31
-94
@@ -20,6 +20,7 @@ const (
|
||||
TaskSwapFace = TaskType("swapFace")
|
||||
TaskUpscale = TaskType("upscale")
|
||||
TaskVariation = TaskType("variation")
|
||||
TaskModal = TaskType("modal") // 局部重绘
|
||||
)
|
||||
|
||||
// MjTask MidJourney 任务
|
||||
@@ -38,49 +39,23 @@ type MjTask struct {
|
||||
ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道
|
||||
Mode string `json:"mode"` // 绘画模式,relax, fast, turbo
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
MaskBase64 string `json:"mask_base64,omitempty"` // 局部重绘蒙版 base64(仅 TaskModal 使用)
|
||||
}
|
||||
|
||||
type SdTask struct {
|
||||
Id int `json:"id"` // job 数据库ID
|
||||
Type TaskType `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
Params SdTaskParams `json:"params"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
type SdTaskParams struct {
|
||||
TaskId string `json:"task_id"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
NegPrompt string `json:"neg_prompt"` // 反向提示词
|
||||
Steps int `json:"steps"` // 迭代步数,默认20
|
||||
Sampler string `json:"sampler"` // 采样器
|
||||
Scheduler string `json:"scheduler"` // 采样调度
|
||||
FaceFix bool `json:"face_fix"` // 面部修复
|
||||
CfgScale float32 `json:"cfg_scale"` //引导系数,默认 7
|
||||
Seed int64 `json:"seed"` // 随机数种子
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
HdFix bool `json:"hd_fix"` // 启用高清修复
|
||||
HdRedrawRate float32 `json:"hd_redraw_rate"` // 高清修复重绘幅度
|
||||
HdScale int `json:"hd_scale"` // 放大倍数
|
||||
HdScaleAlg string `json:"hd_scale_alg"` // 放大算法
|
||||
HdSteps int `json:"hd_steps"` // 高清修复迭代步数
|
||||
}
|
||||
|
||||
// DallTask DALL-E task
|
||||
type DallTask struct {
|
||||
ModelId uint `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
Power int `json:"power"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
// ImageTask Image generation task
|
||||
type ImageTask struct {
|
||||
ModelId uint `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
ModelValue string `json:"model_value"`
|
||||
Image []string `json:"image,omitempty"`
|
||||
Id uint `json:"id"`
|
||||
TaskId string `json:"task_id"` // Kapon 异步任务 ID,与 API 返回的 task_id 一致
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
AspectRatio string `json:"aspect_ratio"`
|
||||
Size string `json:"size"`
|
||||
Power int `json:"power"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
type SunoTask struct {
|
||||
@@ -102,60 +77,22 @@ type SunoTask struct {
|
||||
}
|
||||
|
||||
const (
|
||||
VideoLuma = "luma"
|
||||
VideoRunway = "runway"
|
||||
VideoCog = "cog"
|
||||
VideoKeLing = "keling"
|
||||
VideoLuma = "luma"
|
||||
VideoSora = "sora"
|
||||
VideoVeo = "veo"
|
||||
VideoKeLing = "keling"
|
||||
VideoMiniMax = "minimax"
|
||||
VideoWan = "wan"
|
||||
VideoDoubao = "doubao"
|
||||
)
|
||||
|
||||
type VideoTask struct {
|
||||
Id uint `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
UserId int `json:"user_id"`
|
||||
Type string `json:"type"`
|
||||
TaskId string `json:"task_id"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
Params interface{} `json:"params"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
type LumaVideoParams struct {
|
||||
PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词
|
||||
Loop bool `json:"loop"` // 是否循环参考图
|
||||
StartImgURL string `json:"start_img_url"` // 第一帧参考图地址
|
||||
EndImgURL string `json:"end_img_url"` // 最后一帧参考图地址
|
||||
Model string `json:"model"` // 使用哪个模型生成视频
|
||||
Radio string `json:"radio"` // 视频尺寸
|
||||
Style string `json:"style"` // 风格
|
||||
Duration int `json:"duration"` // 视频时长(秒)
|
||||
}
|
||||
|
||||
type KeLingVideoParams struct {
|
||||
TaskType string `json:"task_type"` // 任务类型: text2video/image2video
|
||||
Model string `json:"model"` // 模型: default/anime
|
||||
Prompt string `json:"prompt"` // 视频描述
|
||||
NegPrompt string `json:"negative_prompt"` // 负面提示词
|
||||
CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1)
|
||||
Mode string `json:"mode"` // 生成模式: std/pro
|
||||
AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1
|
||||
Duration string `json:"duration"` // 视频时长: 5/10
|
||||
CameraControl CameraControl `json:"camera_control"` // 摄像机控制
|
||||
Image string `json:"image"` // 参考图片URL(image2video)
|
||||
ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video)
|
||||
}
|
||||
|
||||
// CameraControl 摄像机控制
|
||||
type CameraControl struct {
|
||||
Type string `json:"type"` // 控制类型: simple/down_back/forward_up/right_turn_forward/left_turn_forward
|
||||
Config CameraConfig `json:"config"` // 控制参数(仅simple类型时使用)
|
||||
}
|
||||
|
||||
// CameraConfig 摄像机参数
|
||||
type CameraConfig struct {
|
||||
Horizontal int `json:"horizontal"` // 水平移动(-10到10)
|
||||
Vertical int `json:"vertical"` // 垂直移动(-10到10)
|
||||
Pan int `json:"pan"` // 左右旋转(-10到10)
|
||||
Tilt int `json:"tilt"` // 上下旋转(-10到10)
|
||||
Roll int `json:"roll"` // 横向翻转(-10到10)
|
||||
Zoom int `json:"zoom"` // 镜头缩放(-10到10)
|
||||
Id uint `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
UserId int `json:"user_id"`
|
||||
Type string `json:"type"` // provider(不带版本号:veo, sora, luma)
|
||||
TaskId string `json:"task_id"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
Params any `json:"params"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import "sync"
|
||||
|
||||
// UserLockManager 提供基于用户ID的TryLock功能,确保同一用户并发请求串行化
|
||||
type UserLockManager struct {
|
||||
mu sync.Mutex
|
||||
locks map[uint]bool
|
||||
}
|
||||
|
||||
func NewUserLockManager() *UserLockManager {
|
||||
return &UserLockManager{mu: sync.Mutex{}, locks: make(map[uint]bool)}
|
||||
}
|
||||
|
||||
// TryLock 尝试为指定用户加锁。若已被占用返回 false
|
||||
func (m *UserLockManager) TryLock(userId uint) bool {
|
||||
if userId == 0 {
|
||||
return true
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.locks[userId] {
|
||||
return false
|
||||
}
|
||||
m.locks[userId] = true
|
||||
return true
|
||||
}
|
||||
|
||||
// Unlock 释放指定用户的锁
|
||||
func (m *UserLockManager) Unlock(userId uint) {
|
||||
if userId == 0 {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
delete(m.locks, userId)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// VideoConfig 视频生成配置(存储在 config 表,name = 'video')
|
||||
type VideoConfig struct {
|
||||
ApiURL string `json:"api_url"` // API 地址
|
||||
ApiKey string `json:"api_key"` // API 密钥
|
||||
VideoPowers map[string]VideoModelPower `json:"video_powers"` // 模型算力配置
|
||||
}
|
||||
|
||||
// VideoModelPower 单个模型的算力配置
|
||||
// PowerConfig 说明:
|
||||
// 新的价格配置方式:根据模型的 priceParams 生成笛卡尔乘积,每个组合对应一个价格
|
||||
// 固定价格示例:{"fixed": 20}
|
||||
// 多参数组合示例:{"5_720P": 10, "5_1080P": 20, "10_720P": 10, "10_1080P": 40}
|
||||
// 复杂组合示例:{"std_5_sound": 10, "std_5_silent": 5, "pro_10_sound": 20, "pro_10_silent": 10}
|
||||
type VideoModelPower struct {
|
||||
Provider string `json:"provider"` // 服务提供商(不带版本号:veo, sora, luma)
|
||||
Model string `json:"model"` // 模型名称(带版本号:veo-2.0, sora-2.0)
|
||||
PowerConfig map[string]int `json:"power_config"` // 算力配置(基于 priceParams 的笛卡尔乘积)
|
||||
ApiKeyType string `json:"api_key_type"` // ApiKey 表的 type 字段(可选,用于多 API Key 场景)
|
||||
}
|
||||
|
||||
// 视频任务状态常量
|
||||
const (
|
||||
VideoStatusPending = "pending" // 等待处理
|
||||
VideoStatusInProgress = "in_progress" // 处理中
|
||||
VideoStatusDownloading = "downloading" // 视频下载中
|
||||
VideoStatusSuccess = "success" // 成功
|
||||
VideoStatusFailed = "failed" // 失败
|
||||
)
|
||||
@@ -36,9 +36,8 @@ const (
|
||||
|
||||
ChPing = WsChannel("ping")
|
||||
ChChat = WsChannel("chat")
|
||||
ChMj = WsChannel("mj")
|
||||
ChSd = WsChannel("sd")
|
||||
ChDall = WsChannel("dall")
|
||||
ChMj = WsChannel("mj")
|
||||
ChImage = WsChannel("image")
|
||||
ChSuno = WsChannel("suno")
|
||||
ChLuma = WsChannel("luma")
|
||||
ChKeLing = WsChannel("keling")
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build_name: runner-build
|
||||
build_log: runner-build-errors.log
|
||||
valid_ext: .go, .tpl, .tmpl, .html
|
||||
no_rebuild_ext: .tpl, .tmpl, .html, .js, .vue
|
||||
ignored: assets, tmp, web, .git, .idea, test, data
|
||||
ignored: assets, tmp, web, .git, .idea, test, data, static
|
||||
build_delay: 600
|
||||
colors: 1
|
||||
log_color_main: cyan
|
||||
|
||||
+33
-24
@@ -1,8 +1,6 @@
|
||||
module geekai
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.4
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.1.0
|
||||
@@ -18,44 +16,56 @@ require (
|
||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.47
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23
|
||||
go.uber.org/zap v1.23.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/driver/mysql v1.4.7
|
||||
)
|
||||
|
||||
require github.com/xxl-job/xxl-job-executor-go v1.2.0
|
||||
|
||||
require (
|
||||
github.com/go-pay/gopay v1.5.101
|
||||
github.com/go-rod/rod v0.116.2
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/go-tika v0.3.1
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3
|
||||
github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/sashabaranov/go-openai v1.38.1
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/image v0.15.0
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49
|
||||
github.com/volcengine/volcengine-go-sdk v1.1.34
|
||||
github.com/xuri/excelize/v2 v2.10.0
|
||||
golang.org/x/image v0.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-pay/crypto v0.0.1 // indirect
|
||||
github.com/go-pay/errgroup v0.0.2 // indirect
|
||||
github.com/go-pay/util v0.0.2 // indirect
|
||||
github.com/go-pay/xlog v0.0.2 // indirect
|
||||
github.com/go-pay/xtime v0.0.2 // indirect
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.0.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mozillazg/go-httpheader v0.2.1 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/ysmood/fetchup v0.3.0 // indirect
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/got v0.40.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -68,7 +78,6 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gaukas/godicttls v0.0.3 // indirect
|
||||
github.com/go-basic/ipv4 v1.0.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
@@ -78,7 +87,7 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
@@ -96,12 +105,12 @@ require (
|
||||
go.uber.org/dig v1.16.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.21.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.5.0
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
@@ -120,8 +129,8 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/fx v1.19.3
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
gorm.io/gorm v1.25.1
|
||||
)
|
||||
|
||||
+139
-41
@@ -1,33 +1,46 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
@@ -39,8 +52,6 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
|
||||
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
@@ -73,8 +84,6 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
@@ -82,20 +91,43 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
|
||||
github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
@@ -115,11 +147,16 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
@@ -127,6 +164,7 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
@@ -134,6 +172,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6 h1:pItMMIM7cHfp43QyuZFeYJyKHPk4yFs8JQUIdjlcWYo=
|
||||
github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6/go.mod h1:X+eOu1OuD+D/IKBa+1HnJBcSaKBB75n5TmKkFVFDPU8=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
@@ -149,12 +189,16 @@ github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRne
|
||||
github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
|
||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
|
||||
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
@@ -179,6 +223,7 @@ github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/H
|
||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0=
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
|
||||
@@ -189,6 +234,11 @@ github.com/quic-go/quic-go v0.45.0 h1:OHmkQGM37luZITyTSu6ff03HP/2IrwDX1ZFiNEhSFU
|
||||
github.com/quic-go/quic-go v0.45.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
|
||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
||||
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
@@ -208,16 +258,28 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49 h1:BQwUw2V21zIRJxstnaxtG/22lBL3+FbUgWhaC6Qd9ws=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49 h1:8mlcG8TmoeEIDQGLYvqc9fdBQrNwKEb56I2HVNy9jdw=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49/go.mod h1:YAdTku4GgK5H2LKea4bPgWf3qFLBibIBzgU1LW5WgEc=
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.47 h1:uoS4Sob16qEYoapkqJq1D1Vnsy9ira9BfNUMtoFYTI4=
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.47/go.mod h1:DH9US8nB+AJXqwu/AMOrCFN1COv3dpytXuJWHgdg7kE=
|
||||
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
|
||||
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||
@@ -230,22 +292,16 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ=
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
|
||||
github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8=
|
||||
github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A=
|
||||
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
|
||||
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
|
||||
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
|
||||
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
|
||||
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
|
||||
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
||||
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
|
||||
github.com/volcengine/volcengine-go-sdk v1.1.34 h1:ha90JycCCTJNCse0UDziBgBsuX98ITOrkwYlDWcm7NI=
|
||||
github.com/volcengine/volcengine-go-sdk v1.1.34/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
|
||||
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@@ -260,8 +316,8 @@ go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
@@ -273,17 +329,25 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -292,14 +356,17 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -315,8 +382,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -333,21 +400,46 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
@@ -360,6 +452,10 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -369,4 +465,6 @@ gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8o
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
|
||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -11,25 +11,27 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/log"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
var logger = log.GetLogger()
|
||||
|
||||
const SuperManagerID = 1
|
||||
const SuperUsername = "admin"
|
||||
|
||||
type ManagerHandler struct {
|
||||
handler.BaseHandler
|
||||
@@ -45,6 +47,26 @@ func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client, cap
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ManagerHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("session", h.Session)
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("enable", h.Enable)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
}
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
var data struct {
|
||||
@@ -59,19 +81,6 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.EnabledVerify {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
|
||||
if res.Error != nil {
|
||||
@@ -85,7 +94,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 超级管理员默认是ID:1
|
||||
if manager.Id != SuperManagerID && manager.Status == false {
|
||||
if manager.Username != SuperUsername && !manager.Status {
|
||||
resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员")
|
||||
return
|
||||
}
|
||||
@@ -116,7 +125,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
IsSuperAdmin bool `json:"is_super_admin"`
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
IsSuperAdmin: manager.Id == 1,
|
||||
IsSuperAdmin: manager.Username == SuperUsername,
|
||||
Token: tokenString,
|
||||
}
|
||||
|
||||
@@ -135,16 +144,15 @@ func (h *ManagerHandler) Logout(c *gin.Context) {
|
||||
|
||||
// Session 会话检测
|
||||
func (h *ManagerHandler) Session(c *gin.Context) {
|
||||
id := h.GetLoginUserId(c)
|
||||
key := fmt.Sprintf("admin/%d", id)
|
||||
if _, err := h.redis.Get(context.Background(), key).Result(); err != nil {
|
||||
resp.NotAuth(c)
|
||||
id := h.GetAdminId(c)
|
||||
if id == 0 {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
return
|
||||
}
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Where("id", id).First(&manager)
|
||||
if res.Error != nil {
|
||||
resp.NotAuth(c)
|
||||
err := h.DB.Where("id", id).First(&manager).Error
|
||||
if err != nil {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -219,12 +227,19 @@ func (h *ManagerHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if id == SuperManagerID {
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("id", id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user.Username == SuperUsername {
|
||||
resp.ERROR(c, "超级管理员不能删除")
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("id", id).Delete(&model.AdminUser{})
|
||||
res = h.DB.Where("id", id).Delete(&model.AdminUser{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
@@ -255,8 +270,14 @@ func (h *ManagerHandler) Enable(c *gin.Context) {
|
||||
|
||||
// ResetPass 重置密码
|
||||
func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
||||
id := h.GetLoginUserId(c)
|
||||
if id != SuperManagerID {
|
||||
id := h.GetAdminId(c)
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("id", id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
if user.Username != SuperUsername {
|
||||
resp.ERROR(c, "只有超级管理员能够进行该操作")
|
||||
return
|
||||
}
|
||||
@@ -270,16 +291,9 @@ func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("id", data.Id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.DB.Updates(&user)
|
||||
res = h.DB.Model(&model.AdminUser{}).Where("id", data.Id).UpdateColumn("password", password)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
|
||||
@@ -10,6 +10,7 @@ package admin
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -30,6 +31,20 @@ func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
|
||||
return &ApiKeyHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ApiKeyHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/apikey/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
|
||||
@@ -8,8 +8,8 @@ package admin
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -30,28 +30,42 @@ func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
|
||||
return &ChatAppHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatAppHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/role/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("sort", h.Sort)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
// Save 创建或者更新某个角色
|
||||
func (h *ChatAppHandler) Save(c *gin.Context) {
|
||||
var data vo.ChatRole
|
||||
var data vo.ChatApp
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var role model.ChatRole
|
||||
var role model.ChatApp
|
||||
err := utils.CopyObject(data, &role)
|
||||
if err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 管理后台创建/编辑的 Gem 均为系统内置
|
||||
role.UserId = 0
|
||||
if data.SystemPrompt != "" {
|
||||
role.SystemPrompt = data.SystemPrompt
|
||||
}
|
||||
role.Id = data.Id
|
||||
if data.CreatedAt > 0 {
|
||||
role.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
} else {
|
||||
err = h.DB.Where("marker", data.Key).First(&role).Error
|
||||
if err == nil {
|
||||
resp.ERROR(c, fmt.Sprintf("角色 %s 已存在", data.Key))
|
||||
return
|
||||
}
|
||||
}
|
||||
err = h.DB.Save(&role).Error
|
||||
if err != nil {
|
||||
@@ -65,9 +79,10 @@ func (h *ChatAppHandler) Save(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
var items []model.ChatRole
|
||||
var roles = make([]vo.ChatRole, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
var items []model.ChatApp
|
||||
var roles = make([]vo.ChatApp, 0)
|
||||
// 仅展示系统内置智能体(user_id = 0),不展示用户自行创建的智能体
|
||||
res := h.DB.Where("user_id = 0").Order("sort_num ASC").Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No data found")
|
||||
return
|
||||
@@ -107,7 +122,7 @@ func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var role vo.ChatRole
|
||||
var role vo.ChatApp
|
||||
err := utils.CopyObject(v, &role)
|
||||
if err == nil {
|
||||
role.Id = v.Id
|
||||
@@ -135,7 +150,7 @@ func (h *ChatAppHandler) Sort(c *gin.Context) {
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
err := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
|
||||
err := h.DB.Model(&model.ChatApp{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -157,7 +172,7 @@ func (h *ChatAppHandler) Set(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
err := h.DB.Model(&model.ChatApp{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -172,9 +187,8 @@ func (h *ChatAppHandler) Remove(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
res := h.DB.Where("id", id).Delete(&model.ChatRole{})
|
||||
res := h.DB.Where("id", id).Delete(&model.ChatApp{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "删除失败!")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -20,6 +22,21 @@ func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler
|
||||
return &ChatAppTypeHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatAppTypeHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/app/type/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
}
|
||||
}
|
||||
|
||||
// Save 创建或更新App类型
|
||||
func (h *ChatAppTypeHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
|
||||
@@ -9,6 +9,7 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -28,16 +29,31 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler {
|
||||
return &ChatHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/chat/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list", h.List)
|
||||
group.POST("message", h.Messages)
|
||||
group.GET("history", h.History)
|
||||
group.GET("remove", h.RemoveChat)
|
||||
group.GET("message/remove", h.RemoveMessage)
|
||||
}
|
||||
}
|
||||
|
||||
type chatItemVo struct {
|
||||
Username string `json:"username"`
|
||||
UserId uint `json:"user_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Title string `json:"title"`
|
||||
Role vo.ChatRole `json:"role"`
|
||||
Model string `json:"model"`
|
||||
Token int `json:"token"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
MsgNum int `json:"msg_num"` // 消息数量
|
||||
Username string `json:"username"`
|
||||
UserId uint `json:"user_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Title string `json:"title"`
|
||||
Role vo.ChatApp `json:"role"`
|
||||
Model string `json:"model"`
|
||||
Token int `json:"token"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
MsgNum int `json:"msg_num"` // 消息数量
|
||||
}
|
||||
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
@@ -87,7 +103,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
}
|
||||
var messages []model.ChatMessage
|
||||
var users []model.User
|
||||
var roles []model.ChatRole
|
||||
var roles []model.ChatApp
|
||||
h.DB.Where("chat_id IN ?", chatIds).Find(&messages)
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
h.DB.Where("id IN ?", roleIds).Find(&roles)
|
||||
@@ -95,7 +111,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
tokenMap := make(map[string]int)
|
||||
userMap := make(map[uint]string)
|
||||
msgMap := make(map[string]int)
|
||||
roleMap := make(map[uint]vo.ChatRole)
|
||||
roleMap := make(map[uint]vo.ChatApp)
|
||||
for _, msg := range messages {
|
||||
tokenMap[msg.ChatId] += msg.Tokens
|
||||
msgMap[msg.ChatId] += 1
|
||||
@@ -104,7 +120,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
userMap[user.Id] = user.Username
|
||||
}
|
||||
for _, r := range roles {
|
||||
var roleVo vo.ChatRole
|
||||
var roleVo vo.ChatApp
|
||||
err := utils.CopyObject(r, &roleVo)
|
||||
if err != nil {
|
||||
continue
|
||||
|
||||
@@ -8,7 +8,9 @@ package admin
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -28,6 +30,22 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||
return &ChatModelHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatModelHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/model/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("batch-remove", h.BatchRemove)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
@@ -201,3 +219,33 @@ func (h *ChatModelHandler) Remove(c *gin.Context) {
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// BatchRemove 批量删除模型
|
||||
func (h *ChatModelHandler) BatchRemove(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if len(data.Ids) == 0 {
|
||||
resp.ERROR(c, "请选择要删除的模型")
|
||||
return
|
||||
}
|
||||
|
||||
// 执行批量删除
|
||||
err := h.DB.Where("id IN ?", data.Ids).Delete(&model.ChatModel{}).Error
|
||||
if err != nil {
|
||||
logger.Error("批量删除模型失败:", err)
|
||||
resp.ERROR(c, "批量删除失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"message": fmt.Sprintf("成功删除 %d 个模型", len(data.Ids)),
|
||||
"deleted_count": len(data.Ids),
|
||||
})
|
||||
}
|
||||
|
||||
+360
-152
@@ -8,108 +8,396 @@ package admin
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/store"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sms"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
handler.BaseHandler
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
sysConfig *types.SystemConfig
|
||||
alipayService *payment.AlipayService
|
||||
wxpayService *payment.WxPayService
|
||||
epayService *payment.EPayService
|
||||
smsManager *sms.SmsManager
|
||||
uploaderManager *oss.UploaderManager
|
||||
smtpService *service.SmtpService
|
||||
captchaService *service.CaptchaService
|
||||
wxLoginService *service.WxLoginService
|
||||
wxGzhService *service.WxGzhService
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService) *ConfigHandler {
|
||||
func NewConfigHandler(
|
||||
app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
sysConfig *types.SystemConfig,
|
||||
alipayService *payment.AlipayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
epayService *payment.EPayService,
|
||||
smsManager *sms.SmsManager,
|
||||
uploaderManager *oss.UploaderManager,
|
||||
smtpService *service.SmtpService,
|
||||
captchaService *service.CaptchaService,
|
||||
wxLoginService *service.WxLoginService,
|
||||
wxChatService *service.WxGzhService,
|
||||
) *ConfigHandler {
|
||||
return &ConfigHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
levelDB: levelDB,
|
||||
licenseService: licenseService,
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
sysConfig: sysConfig,
|
||||
alipayService: alipayService,
|
||||
wxpayService: wxpayService,
|
||||
epayService: epayService,
|
||||
smsManager: smsManager,
|
||||
uploaderManager: uploaderManager,
|
||||
smtpService: smtpService,
|
||||
captchaService: captchaService,
|
||||
wxLoginService: wxLoginService,
|
||||
wxGzhService: wxChatService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
Config struct {
|
||||
types.SystemConfig
|
||||
Content string `json:"content,omitempty"`
|
||||
Updated bool `json:"updated,omitempty"`
|
||||
} `json:"config"`
|
||||
ConfigBak types.SystemConfig `json:"config_bak,omitempty"`
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ConfigHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/admin/config")
|
||||
|
||||
// 需要管理员登录的接口
|
||||
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
rg.POST("update/base", h.UpdateBase)
|
||||
rg.POST("update/power", h.UpdatePower)
|
||||
rg.POST("update/notice", h.UpdateNotice)
|
||||
rg.POST("update/agreement", h.UpdateAgreement)
|
||||
rg.POST("update/privacy", h.UpdatePrivacy)
|
||||
rg.POST("update/mark_map", h.UpdateMarkMap)
|
||||
rg.POST("update/captcha", h.UpdateCaptcha)
|
||||
rg.POST("update/wx_login", h.UpdateWxLogin)
|
||||
rg.POST("update/payment", h.UpdatePayment)
|
||||
rg.POST("update/sms", h.UpdateSms)
|
||||
rg.POST("update/oss", h.UpdateOss)
|
||||
rg.POST("update/smtp", h.UpdateStmp)
|
||||
rg.GET("get", h.Get)
|
||||
rg.POST("update/wx_gzh", h.UpdateWxGzh)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateBase 更新基础配置
|
||||
func (h *ConfigHandler) UpdateBase(c *gin.Context) {
|
||||
var data types.BaseConfig
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
logger.Errorf("Update config failed: %v", err)
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// ONLY authorized user can change the copyright
|
||||
if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy {
|
||||
resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权")
|
||||
err := h.Update(types.ConfigKeySystem, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 如果要启用图形验证码功能,则检查是否配置了 API 服务
|
||||
if data.Config.EnabledVerify && h.App.Config.ApiConfig.AppId == "" {
|
||||
resp.ERROR(c, "启用验证码服务需要先配置 GeekAI 官方 API 服务 AppId 和 Token")
|
||||
return
|
||||
}
|
||||
h.sysConfig.Base = data
|
||||
|
||||
value := utils.JsonEncode(&data.Config)
|
||||
config := model.Config{Key: data.Key, Config: value}
|
||||
res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if config.Id > 0 {
|
||||
config.Config = value
|
||||
res := h.DB.Updates(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// update config cache for AppServer
|
||||
var cfg model.Config
|
||||
h.DB.Where("marker", data.Key).First(&cfg)
|
||||
var err error
|
||||
if data.Key == "system" {
|
||||
err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to update config cache: "+err.Error())
|
||||
return
|
||||
}
|
||||
logger.Infof("Update AppServer's config successfully: %v", config.Config)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, config)
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
// UpdatePower 更新系统配置
|
||||
func (h *ConfigHandler) UpdatePower(c *gin.Context) {
|
||||
var data struct {
|
||||
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||
MjUpscalePower int `json:"mj_upscale_power,omitempty"` // MJ 放大/变换消耗算力
|
||||
MjBlendPower int `json:"mj_blend_power,omitempty"` // MJ 融图消耗算力
|
||||
MjSwapFacePower int `json:"mj_swap_face_power,omitempty"` // MJ 换脸消耗算力
|
||||
MjModalPower int `json:"mj_modal_power,omitempty"` // MJ 局部重绘消耗算力
|
||||
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
|
||||
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]interface{}
|
||||
err := utils.JsonDecode(config.Config, &value)
|
||||
h.sysConfig.Base.InitPower = data.InitPower
|
||||
h.sysConfig.Base.DailyPower = data.DailyPower
|
||||
h.sysConfig.Base.InvitePower = data.InvitePower
|
||||
h.sysConfig.Base.MjPower = data.MjPower
|
||||
h.sysConfig.Base.MjActionPower = data.MjActionPower
|
||||
h.sysConfig.Base.MjUpscalePower = data.MjUpscalePower
|
||||
h.sysConfig.Base.MjBlendPower = data.MjBlendPower
|
||||
h.sysConfig.Base.MjSwapFacePower = data.MjSwapFacePower
|
||||
h.sysConfig.Base.MjModalPower = data.MjModalPower
|
||||
h.sysConfig.Base.SunoPower = data.SunoPower
|
||||
h.sysConfig.Base.LumaPower = data.LumaPower
|
||||
h.sysConfig.Base.KeLingPowers = data.KeLingPowers
|
||||
|
||||
err := h.Update(types.ConfigKeySystem, h.sysConfig.Base)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, h.sysConfig.Base)
|
||||
}
|
||||
|
||||
// UpdateNotice 更新公告配置
|
||||
func (h *ConfigHandler) UpdateNotice(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyNotice, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateAgreement 更新用户协议配置
|
||||
func (h *ConfigHandler) UpdateAgreement(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyAgreement, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdatePrivacy 更新隐私政策配置
|
||||
func (h *ConfigHandler) UpdatePrivacy(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyPrivacy, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateMarkMap 更新思维导图配置
|
||||
func (h *ConfigHandler) UpdateMarkMap(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyMarkMap, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateCaptcha 更新行为验证码配置
|
||||
func (h *ConfigHandler) UpdateCaptcha(c *gin.Context) {
|
||||
var data types.CaptchaConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyCaptcha, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.captchaService.UpdateConfig(data)
|
||||
resp.SUCCESS(c, data)
|
||||
|
||||
}
|
||||
|
||||
// UpdatePayment 更新支付配置
|
||||
func (h *ConfigHandler) UpdatePayment(c *gin.Context) {
|
||||
var data types.PaymentConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyPayment, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 如果启用状态发生改变,则需要更新支付服务配置
|
||||
if data.WxPay.Enabled {
|
||||
err = h.wxpayService.UpdateConfig(&data.WxPay)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if data.Epay.Enabled {
|
||||
h.epayService.UpdateConfig(&data.Epay)
|
||||
}
|
||||
if data.Alipay.Enabled {
|
||||
err = h.alipayService.UpdateConfig(&data.Alipay)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.sysConfig.Payment = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateSms 更新短信配置
|
||||
func (h *ConfigHandler) UpdateSms(c *gin.Context) {
|
||||
var data types.SMSConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeySms, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新服务配置
|
||||
h.smsManager.UpdateConfig(data)
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateOss 更新 Oss 配置
|
||||
func (h *ConfigHandler) UpdateOss(c *gin.Context) {
|
||||
var data types.OSSConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyOss, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新服务配置
|
||||
h.uploaderManager.UpdateConfig(data)
|
||||
h.sysConfig.OSS = data
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateStmp 更新 Stmp 配置
|
||||
func (h *ConfigHandler) UpdateStmp(c *gin.Context) {
|
||||
var data types.SmtpConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeySmtp, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新服务配置
|
||||
h.smtpService.UpdateConfig(&data)
|
||||
h.sysConfig.SMTP = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateWxLogin 更新微信登录配置
|
||||
func (h *ConfigHandler) UpdateWxLogin(c *gin.Context) {
|
||||
var data types.WxLoginConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
err := h.Update(types.ConfigKeyWxLogin, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.wxLoginService.UpdateConfig(data)
|
||||
h.sysConfig.WxLogin = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// Update 更新系统配置
|
||||
func (h *ConfigHandler) Update(name string, value any) error {
|
||||
var config model.Config
|
||||
err := h.DB.Where("name", name).First(&config).Error
|
||||
if err != nil { // 不存在则创建
|
||||
config.Name = name
|
||||
config.Value = utils.JsonEncode(value)
|
||||
return h.DB.Create(&config).Error
|
||||
} else { // 存在则更新
|
||||
config.Value = utils.JsonEncode(value)
|
||||
return h.DB.Updates(&config).Error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Get 获取指定名称的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
name := c.Query("key")
|
||||
var config model.Config
|
||||
err := h.DB.Where("name", name).First(&config).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
resp.SUCCESS(c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]any
|
||||
err = utils.JsonDecode(config.Value, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -118,99 +406,19 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// Active 激活系统
|
||||
func (h *ConfigHandler) Active(c *gin.Context) {
|
||||
var data struct {
|
||||
License string `json:"license"`
|
||||
}
|
||||
func (h *ConfigHandler) UpdateWxGzh(c *gin.Context) {
|
||||
var data types.WxGzhConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
info, err := host.Info()
|
||||
err := h.Update(types.ConfigKeyWxGzh, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = h.licenseService.ActiveLicense(data.License, info.HostID)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
|
||||
}
|
||||
|
||||
// GetLicense 获取 License 信息
|
||||
func (h *ConfigHandler) GetLicense(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license)
|
||||
}
|
||||
|
||||
// FixData 修复数据
|
||||
func (h *ConfigHandler) FixData(c *gin.Context) {
|
||||
resp.ERROR(c, "当前升级版本没有数据需要修正!")
|
||||
//var fixed bool
|
||||
//version := "data_fix_4.1.4"
|
||||
//err := h.levelDB.Get(version, &fixed)
|
||||
//if err == nil || fixed {
|
||||
// resp.ERROR(c, "当前版本数据修复已完成,请不要重复执行操作")
|
||||
// return
|
||||
//}
|
||||
//tx := h.DB.Begin()
|
||||
//var users []model.User
|
||||
//err = tx.Find(&users).Error
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//for _, user := range users {
|
||||
// if user.Email != "" || user.Mobile != "" {
|
||||
// continue
|
||||
// }
|
||||
// if utils.IsValidEmail(user.Username) {
|
||||
// user.Email = user.Username
|
||||
// } else if utils.IsValidMobile(user.Username) {
|
||||
// user.Mobile = user.Username
|
||||
// }
|
||||
// err = tx.Save(&user).Error
|
||||
// if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// tx.Rollback()
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//var orders []model.Order
|
||||
//err = h.DB.Find(&orders).Error
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//for _, order := range orders {
|
||||
// if order.PayWay == "支付宝" {
|
||||
// order.PayWay = "alipay"
|
||||
// order.PayType = "alipay"
|
||||
// } else if order.PayWay == "微信支付" {
|
||||
// order.PayWay = "wechat"
|
||||
// order.PayType = "wxpay"
|
||||
// } else if order.PayWay == "hupi" {
|
||||
// order.PayType = "wxpay"
|
||||
// }
|
||||
// err = tx.Save(&order).Error
|
||||
// if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// tx.Rollback()
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
//tx.Commit()
|
||||
//err = h.levelDB.Put(version, true)
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//resp.SUCCESS(c)
|
||||
h.wxGzhService.UpdateConfig(data)
|
||||
h.sysConfig.WxGzh = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ import (
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DashboardHandler struct {
|
||||
@@ -27,46 +28,159 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
|
||||
return &DashboardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *DashboardHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/dashboard/")
|
||||
group.GET("stats", h.Stats)
|
||||
}
|
||||
|
||||
// statsVo 增加 recentOrders、recentUsers 字段
|
||||
// 最近订单
|
||||
type OrderBrief struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// 最近用户
|
||||
type UserBrief struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
LastActive time.Time `json:"last_active"`
|
||||
}
|
||||
|
||||
type statsVo struct {
|
||||
Users int64 `json:"users"`
|
||||
Chats int64 `json:"chats"`
|
||||
Tokens int `json:"tokens"`
|
||||
Income float64 `json:"income"`
|
||||
Chart map[string]map[string]float64 `json:"chart"`
|
||||
Users int64 `json:"users"`
|
||||
Chats int64 `json:"chats"`
|
||||
Tokens int `json:"tokens"`
|
||||
Income float64 `json:"income"`
|
||||
Chart map[string]map[string]float64 `json:"chart"`
|
||||
TodayUsers int64 `json:"todayUsers"`
|
||||
TodayChats int64 `json:"todayChats"`
|
||||
TodayTokens int `json:"todayTokens"`
|
||||
TodayIncome float64 `json:"todayIncome"`
|
||||
TodayOrders int64 `json:"todayOrders"`
|
||||
TodayImageJobs int64 `json:"todayImageJobs"`
|
||||
TodayVideoJobs int64 `json:"todayVideoJobs"`
|
||||
TodayMusicJobs int64 `json:"todayMusicJobs"`
|
||||
Orders int64 `json:"orders"`
|
||||
ImageJobs int64 `json:"imageJobs"`
|
||||
VideoJobs int64 `json:"videoJobs"`
|
||||
MusicJobs int64 `json:"musicJobs"`
|
||||
RecentOrders []OrderBrief `json:"recentOrders"`
|
||||
RecentUsers []UserBrief `json:"recentUsers"`
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
stats := statsVo{}
|
||||
// new users statistic
|
||||
var userCount int64
|
||||
now := time.Now()
|
||||
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
res := h.DB.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount)
|
||||
if res.Error == nil {
|
||||
stats.Users = userCount
|
||||
|
||||
// 总用户数
|
||||
h.DB.Model(&model.User{}).Count(&stats.Users)
|
||||
|
||||
// 今日新增用户
|
||||
h.DB.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&stats.TodayUsers)
|
||||
|
||||
// 总对话数
|
||||
h.DB.Model(&model.ChatItem{}).Count(&stats.Chats)
|
||||
|
||||
// 今日新增对话
|
||||
h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&stats.TodayChats)
|
||||
|
||||
// 总算力消耗
|
||||
var powerLogs []model.PowerLog
|
||||
h.DB.Where("mark = ?", types.PowerSub).Find(&powerLogs)
|
||||
for _, item := range powerLogs {
|
||||
stats.Tokens += item.Amount
|
||||
}
|
||||
|
||||
// new chats statistic
|
||||
var chatCount int64
|
||||
res = h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
|
||||
if res.Error == nil {
|
||||
stats.Chats = chatCount
|
||||
// 今日算力消耗
|
||||
var todayPowerLogs []model.PowerLog
|
||||
h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", zeroTime).Find(&todayPowerLogs)
|
||||
for _, item := range todayPowerLogs {
|
||||
stats.TodayTokens += item.Amount
|
||||
}
|
||||
|
||||
// tokens took stats
|
||||
var historyMessages []model.ChatMessage
|
||||
res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
stats.Tokens += item.Tokens
|
||||
}
|
||||
|
||||
// 订单收入
|
||||
var orders []model.Order
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
|
||||
for _, item := range orders {
|
||||
// 总收入
|
||||
var allOrders []model.Order
|
||||
h.DB.Where("status = ?", types.OrderPaidSuccess).Find(&allOrders)
|
||||
for _, item := range allOrders {
|
||||
stats.Income += item.Amount
|
||||
}
|
||||
|
||||
// 今日收入
|
||||
var todayOrders []model.Order
|
||||
h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&todayOrders)
|
||||
for _, item := range todayOrders {
|
||||
stats.TodayIncome += item.Amount
|
||||
}
|
||||
|
||||
// 订单总数
|
||||
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Count(&stats.Orders)
|
||||
|
||||
// 今日订单数
|
||||
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Count(&stats.TodayOrders)
|
||||
|
||||
// 图片生成任务统计
|
||||
var mjJobs, imageJobs, jimengImageJobs int64
|
||||
h.DB.Model(&model.MidJourneyJob{}).Count(&mjJobs)
|
||||
h.DB.Model(&model.ImageJob{}).Count(&imageJobs)
|
||||
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Count(&jimengImageJobs)
|
||||
stats.ImageJobs = mjJobs + imageJobs + jimengImageJobs
|
||||
|
||||
logger.Info("stats.ImageJobs", stats.ImageJobs)
|
||||
|
||||
// 今日图片生成任务统计
|
||||
var todayMjJobs, todayImageJobs, todayJimengImageJobs int64
|
||||
h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs)
|
||||
h.DB.Model(&model.ImageJob{}).Where("created_at > ?", zeroTime).Count(&todayImageJobs)
|
||||
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Where("created_at > ?", zeroTime).Count(&todayJimengImageJobs)
|
||||
stats.TodayImageJobs = todayMjJobs + todayImageJobs + todayJimengImageJobs
|
||||
|
||||
// 视频生成任务统计
|
||||
var videoJobs, jimengVideoJobs int64
|
||||
h.DB.Model(&model.VideoJob{}).Count(&videoJobs)
|
||||
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_video", "image_to_video"}).Count(&jimengVideoJobs)
|
||||
stats.VideoJobs = videoJobs + jimengVideoJobs
|
||||
|
||||
// 今日视频生成任务统计
|
||||
var todayVideoJobs, todayJimengVideoJobs int64
|
||||
h.DB.Model(&model.VideoJob{}).Where("created_at > ?", zeroTime).Count(&todayVideoJobs)
|
||||
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_video", "image_to_video"}).Where("created_at > ?", zeroTime).Count(&todayJimengVideoJobs)
|
||||
stats.TodayVideoJobs = todayVideoJobs + todayJimengVideoJobs
|
||||
|
||||
// 音乐生成任务统计
|
||||
h.DB.Model(&model.SunoJob{}).Count(&stats.MusicJobs)
|
||||
|
||||
// 今日音乐生成任务统计
|
||||
h.DB.Model(&model.SunoJob{}).Where("created_at > ?", zeroTime).Count(&stats.TodayMusicJobs)
|
||||
|
||||
// recentOrders: 最近10条已支付订单
|
||||
var orderList []model.Order
|
||||
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Order("created_at desc").Limit(10).Find(&orderList)
|
||||
for _, o := range orderList {
|
||||
stats.RecentOrders = append(stats.RecentOrders, OrderBrief{
|
||||
OrderNo: o.OrderNo,
|
||||
Amount: o.Amount,
|
||||
CreatedAt: o.CreatedAt,
|
||||
})
|
||||
}
|
||||
// recentUsers: 最近10个注册用户
|
||||
var userList []model.User
|
||||
h.DB.Model(&model.User{}).Order("created_at desc").Limit(10).Find(&userList)
|
||||
for _, u := range userList {
|
||||
lastActive := u.UpdatedAt
|
||||
if lastActive.IsZero() {
|
||||
lastActive = u.CreatedAt
|
||||
}
|
||||
stats.RecentUsers = append(stats.RecentUsers, UserBrief{
|
||||
Nickname: u.Nickname,
|
||||
Avatar: u.Avatar,
|
||||
LastActive: lastActive,
|
||||
})
|
||||
}
|
||||
|
||||
// 统计7天的订单的图表
|
||||
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
|
||||
var statsChart = make(map[string]map[string]float64)
|
||||
@@ -81,23 +195,29 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
|
||||
// 统计用户7天增加的曲线
|
||||
var users []model.User
|
||||
res = h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users)
|
||||
if res.Error == nil {
|
||||
err := h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users).Error
|
||||
if err == nil {
|
||||
for _, item := range users {
|
||||
userStatistic[item.CreatedAt.Format("2006-01-02")] += 1
|
||||
}
|
||||
}
|
||||
|
||||
// 统计7天Token 消耗
|
||||
res = h.DB.Where("created_at > ?", startDate).Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens)
|
||||
// 统计7天算力消耗
|
||||
var chartPowerLogs []model.PowerLog
|
||||
err = h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", startDate).Find(&chartPowerLogs).Error
|
||||
if err == nil {
|
||||
for _, item := range chartPowerLogs {
|
||||
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计最近7天的订单
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
|
||||
for _, item := range orders {
|
||||
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
||||
var orders []model.Order
|
||||
err = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders).Error
|
||||
if err == nil {
|
||||
for _, item := range orders {
|
||||
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
||||
}
|
||||
}
|
||||
|
||||
statsChart["users"] = userStatistic
|
||||
|
||||
@@ -9,6 +9,7 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
@@ -30,6 +31,21 @@ func NewFunctionHandler(app *core.AppServer, db *gorm.DB) *FunctionHandler {
|
||||
return &FunctionHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *FunctionHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/function/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("token", h.GenToken)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) Save(c *gin.Context) {
|
||||
var data vo.Function
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
@@ -119,7 +135,6 @@ func (h *FunctionHandler) GenToken(c *gin.Context) {
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
logger.Error("error with generate token", err)
|
||||
resp.ERROR(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ package admin
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
@@ -33,6 +34,19 @@ func NewImageHandler(app *core.AppServer, db *gorm.DB, userService *service.User
|
||||
return &ImageHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ImageHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/image/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list/mj", h.MjList)
|
||||
group.POST("list/image", h.ImageList)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
type imageQuery struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Username string `json:"username"`
|
||||
@@ -85,8 +99,8 @@ func (h *ImageHandler) MjList(c *gin.Context) {
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
// SdList Stable Diffusion 任务列表
|
||||
func (h *ImageHandler) SdList(c *gin.Context) {
|
||||
// ImageList Image generation 任务列表
|
||||
func (h *ImageHandler) ImageList(c *gin.Context) {
|
||||
var data imageQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -108,59 +122,15 @@ func (h *ImageHandler) SdList(c *gin.Context) {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.SdJob{}).Count(&total)
|
||||
var list []model.SdJob
|
||||
var items = make([]vo.SdJob, 0)
|
||||
session.Model(&model.ImageJob{}).Count(&total)
|
||||
var list []model.ImageJob
|
||||
var items = make([]vo.ImageJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.SdJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
// DallList DALL-E 任务列表
|
||||
func (h *ImageHandler) DallList(c *gin.Context) {
|
||||
var data imageQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
if err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.DallJob{}).Count(&total)
|
||||
var list []model.DallJob
|
||||
var items = make([]vo.DallJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.DallJob
|
||||
var job vo.ImageJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -194,9 +164,8 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
case "sd":
|
||||
var job model.SdJob
|
||||
case "image":
|
||||
var job model.ImageJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
@@ -204,29 +173,12 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
md = "stable-diffusion"
|
||||
md = "image-generation"
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
case "dall":
|
||||
var job model.DallJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
md = "dall-e-3"
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AdminJimengHandler 管理后台即梦AI处理器
|
||||
type AdminJimengHandler struct {
|
||||
handler.BaseHandler
|
||||
jimengClient *jimeng.Client
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
// NewAdminJimengHandler 创建管理后台即梦AI处理器
|
||||
func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengClient *jimeng.Client, userService *service.UserService, uploader *oss.UploaderManager) *AdminJimengHandler {
|
||||
return &AdminJimengHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
jimengClient: jimengClient,
|
||||
userService: userService,
|
||||
uploader: uploader,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册即梦AI管理后台路由
|
||||
func (h *AdminJimengHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/admin/jimeng/")
|
||||
rg.GET("/jobs", h.Jobs)
|
||||
rg.GET("/jobs/:id", h.JobDetail)
|
||||
rg.POST("/jobs/remove", h.BatchRemove)
|
||||
rg.GET("/stats", h.Stats)
|
||||
rg.POST("/config/update", h.UpdateConfig)
|
||||
}
|
||||
|
||||
// Jobs 获取任务列表
|
||||
func (h *AdminJimengHandler) Jobs(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
taskType := h.GetTrim(c, "type")
|
||||
status := h.GetTrim(c, "status")
|
||||
|
||||
var tasks []model.JimengJob
|
||||
var total int64
|
||||
|
||||
session := h.DB.Model(&model.JimengJob{})
|
||||
|
||||
// 构建查询条件
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if taskType != "" {
|
||||
session = session.Where("type = ?", taskType)
|
||||
}
|
||||
if status != "" {
|
||||
session = session.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
err := session.Count(&total).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取任务数量失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = session.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tasks).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取任务列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"jobs": tasks,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// JobDetail 获取任务详情
|
||||
func (h *AdminJimengHandler) JobDetail(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
jobId, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var job model.JimengJob
|
||||
err = h.DB.Where("id = ?", jobId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// BatchRemove 批量删除任务
|
||||
func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
|
||||
var req struct {
|
||||
JobIds []uint `json:"job_ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var deletedCount int64 = 0
|
||||
for _, jobId := range req.JobIds {
|
||||
var job model.JimengJob
|
||||
err := h.DB.Where("id = ?", jobId).First(&job).Error
|
||||
if err != nil {
|
||||
continue // 跳过不存在的
|
||||
}
|
||||
tx := h.DB.Begin()
|
||||
if job.Status != types.JMTaskStatusSuccess && job.Power > 0 {
|
||||
remark := fmt.Sprintf("任务未成功,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "jimeng",
|
||||
Remark: remark,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
}
|
||||
err = tx.Where("id = ?", jobId).Delete(&model.JimengJob{}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
tx.Commit()
|
||||
deletedCount++
|
||||
if job.ImgURL != "" {
|
||||
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
}
|
||||
if job.VideoURL != "" {
|
||||
err = h.uploader.GetUploadHandler().Delete(job.VideoURL)
|
||||
if err != nil {
|
||||
logger.Error("remove video failed: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"message": "批量删除成功",
|
||||
"deleted_count": deletedCount,
|
||||
})
|
||||
}
|
||||
|
||||
// Stats 获取统计信息
|
||||
func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
||||
type StatResult struct {
|
||||
Status types.JMTaskStatus `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
var stats []StatResult
|
||||
err := h.DB.Model(&model.JimengJob{}).
|
||||
Select("status, COUNT(*) as count").
|
||||
Group("status").
|
||||
Find(&stats).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取统计信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 整理统计数据
|
||||
result := gin.H{
|
||||
"totalTasks": int64(0),
|
||||
"completedTasks": int64(0),
|
||||
"processingTasks": int64(0),
|
||||
"failedTasks": int64(0),
|
||||
"pendingTasks": int64(0),
|
||||
}
|
||||
|
||||
for _, stat := range stats {
|
||||
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
|
||||
switch stat.Status {
|
||||
case types.JMTaskStatusInQueue:
|
||||
result["pendingTasks"] = stat.Count
|
||||
case types.JMTaskStatusSuccess:
|
||||
result["completedTasks"] = stat.Count
|
||||
case types.JMTaskStatusGenerating:
|
||||
result["processingTasks"] = stat.Count
|
||||
case types.JMTaskStatusFailed:
|
||||
result["failedTasks"] = stat.Count
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, result)
|
||||
}
|
||||
|
||||
// UpdateConfig 更新即梦AI配置
|
||||
func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
|
||||
var req types.JimengConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.AccessKey == "" {
|
||||
resp.ERROR(c, "AccessKey不能为空")
|
||||
return
|
||||
}
|
||||
if req.SecretKey == "" {
|
||||
resp.ERROR(c, "SecretKey不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证算力配置
|
||||
if len(req.Powers) == 0 {
|
||||
resp.ERROR(c, "请至少配置一个模型的积分")
|
||||
return
|
||||
}
|
||||
for key, val := range req.Powers {
|
||||
if val <= 0 {
|
||||
resp.ERROR(c, fmt.Sprintf("模型 %s 的积分必须大于0", key))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
tx := h.DB.Begin()
|
||||
value := utils.JsonEncode(&req)
|
||||
var exist model.Config
|
||||
tx.Where("name", types.ConfigKeyJimeng).First(&exist)
|
||||
|
||||
if exist.Id > 0 {
|
||||
exist.Value = value
|
||||
err := tx.Updates(&exist).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
exist.Name = types.ConfigKeyJimeng
|
||||
exist.Value = value
|
||||
err := tx.Create(&exist).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "创建配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 更新服务中的客户端配置
|
||||
err := h.jimengClient.UpdateConfig(req)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
h.App.SysConfig.Jimeng = req
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MediaHandler struct {
|
||||
handler.BaseHandler
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *MediaHandler {
|
||||
return &MediaHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
|
||||
}
|
||||
|
||||
type mediaQuery struct {
|
||||
Type string `json:"type"` // 任务类型 luma, keling
|
||||
Prompt string `json:"prompt"`
|
||||
Username string `json:"username"`
|
||||
CreatedAt []string `json:"created_at"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// SunoList Suno 任务列表
|
||||
func (h *MediaHandler) SunoList(c *gin.Context) {
|
||||
var data mediaQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
if err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.SunoJob{}).Count(&total)
|
||||
var list []model.SunoJob
|
||||
var items = make([]vo.SunoJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.SunoJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
// Videos 视频任务列表
|
||||
func (h *MediaHandler) Videos(c *gin.Context) {
|
||||
var data mediaQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{}).Where("type", data.Type)
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
if err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.VideoJob{}).Count(&total)
|
||||
var list []model.VideoJob
|
||||
var items = make([]vo.VideoJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.VideoJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
if job.VideoURL == "" {
|
||||
job.VideoURL = job.WaterURL
|
||||
}
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
func (h *MediaHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
tab := c.Query("tab")
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var md, remark, fileURL string
|
||||
var power, userId, progress int
|
||||
switch tab {
|
||||
case "suno":
|
||||
var job model.SunoJob
|
||||
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
tx.Delete(&job)
|
||||
md = "suno"
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
fileURL = job.AudioURL
|
||||
break
|
||||
case "luma":
|
||||
case "keling":
|
||||
var job model.VideoJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
md = job.Type
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
fileURL = job.VideoURL
|
||||
if fileURL == "" {
|
||||
fileURL = job.WaterURL
|
||||
}
|
||||
break
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if progress != 100 {
|
||||
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(fileURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -27,6 +27,16 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
return &MenuHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MenuHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/menu/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service/moderation"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ModerationHandler struct {
|
||||
handler.BaseHandler
|
||||
sysConfig *types.SystemConfig
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewModerationHandler(app *core.AppServer, db *gorm.DB, sysConfig *types.SystemConfig, moderationManager *moderation.ServiceManager) *ModerationHandler {
|
||||
return &ModerationHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, sysConfig: sysConfig, moderationManager: moderationManager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ModerationHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/moderation/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("batch-remove", h.BatchRemove)
|
||||
group.GET("source-list", h.GetSourceList)
|
||||
group.POST("config", h.UpdateModeration)
|
||||
group.POST("test", h.TestModeration)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取文本审核记录列表
|
||||
func (h *ModerationHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Source string `json:"source"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
|
||||
// 构建查询条件
|
||||
if data.Username != "" {
|
||||
// 通过用户名查找用户ID
|
||||
var user model.User
|
||||
if err := h.DB.Where("username LIKE ?", "%"+data.Username+"%").First(&user).Error; err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
|
||||
if data.Source != "" {
|
||||
session = session.Where("source", data.Source)
|
||||
}
|
||||
|
||||
if data.StartDate != "" && data.EndDate != "" {
|
||||
startTime := data.StartDate + " 00:00:00"
|
||||
endTime := data.EndDate + " 23:59:59"
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", startTime, endTime)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.Moderation{}).Count(&total)
|
||||
|
||||
// 分页
|
||||
page := data.Page
|
||||
pageSize := data.PageSize
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
|
||||
// 查询数据
|
||||
var items []model.Moderation
|
||||
err := session.Order("id DESC").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
userIds := make([]uint, 0)
|
||||
for _, item := range items {
|
||||
userIds = append(userIds, item.UserId)
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
if len(userIds) > 0 {
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
}
|
||||
|
||||
userMap := make(map[uint]string)
|
||||
for _, user := range users {
|
||||
userMap[user.Id] = user.Username
|
||||
}
|
||||
|
||||
// 转换为响应数据
|
||||
list := make([]map[string]any, 0)
|
||||
for _, item := range items {
|
||||
var moderation types.ModerationResult
|
||||
err := utils.JsonDecode(item.Result, &moderation)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var result []string
|
||||
for value, label := range types.ModerationCategories {
|
||||
if moderation.Categories[value] {
|
||||
result = append(result, label)
|
||||
}
|
||||
}
|
||||
list = append(list, map[string]any{
|
||||
"id": item.Id,
|
||||
"user_id": item.UserId,
|
||||
"username": userMap[item.UserId],
|
||||
"source": item.Source,
|
||||
"input": item.Input,
|
||||
"output": item.Output,
|
||||
"result": result,
|
||||
"created_at": item.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, map[string]any{
|
||||
"items": list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ModerationHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Where("id", id).Delete(&model.Moderation{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// BatchRemove 批量删除文本审核记录
|
||||
func (h *ModerationHandler) BatchRemove(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if len(data.Ids) == 0 {
|
||||
resp.ERROR(c, "请选择要删除的记录")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Where("id IN ?", data.Ids).Delete(&model.Moderation{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// 获取 source 列表
|
||||
func (h *ModerationHandler) GetSourceList(c *gin.Context) {
|
||||
sources := []gin.H{
|
||||
{
|
||||
"id": types.ModerationSourceChat,
|
||||
"name": "AI对话",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceMJ,
|
||||
"name": "Midjourney 绘图",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceImage,
|
||||
"name": "AI图像生成",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceSuno,
|
||||
"name": "Suno 音乐",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceVideo,
|
||||
"name": "视频生成",
|
||||
},
|
||||
{
|
||||
"id": types.ModerationSourceJiMeng,
|
||||
"name": "即梦AI",
|
||||
},
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, sources)
|
||||
}
|
||||
|
||||
// UpdateModeration 更新文本审查配置
|
||||
func (h *ModerationHandler) UpdateModeration(c *gin.Context) {
|
||||
var data types.ModerationConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var config model.Config
|
||||
err := h.DB.Where("name", types.ConfigKeyModeration).First(&config).Error
|
||||
if err != nil {
|
||||
config.Name = types.ConfigKeyModeration
|
||||
config.Value = utils.JsonEncode(data)
|
||||
err = h.DB.Create(&config).Error
|
||||
} else {
|
||||
config.Value = utils.JsonEncode(data)
|
||||
err = h.DB.Updates(&config).Error
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.moderationManager.UpdateConfig(data)
|
||||
h.sysConfig.Moderation = data
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// 测试结果类型,用于前端显示
|
||||
type ModerationTestResult struct {
|
||||
IsAbnormal bool `json:"isAbnormal"`
|
||||
Details []ModerationTestDetail `json:"details"`
|
||||
}
|
||||
|
||||
type ModerationTestDetail struct {
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description"`
|
||||
Confidence string `json:"confidence"`
|
||||
IsCategory bool `json:"isCategory"`
|
||||
}
|
||||
|
||||
// TestModeration 测试文本审查服务
|
||||
func (h *ModerationHandler) TestModeration(c *gin.Context) {
|
||||
var data struct {
|
||||
Text string `json:"text"`
|
||||
Service string `json:"service"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Text == "" {
|
||||
resp.ERROR(c, "测试文本不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否启用了文本审查
|
||||
if !h.sysConfig.Moderation.Enable {
|
||||
resp.ERROR(c, "文本审查服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前激活的审核服务
|
||||
service := h.moderationManager.GetService()
|
||||
// 执行文本审核
|
||||
result, err := service.Moderate(data.Text)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "审核服务调用失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为前端需要的格式
|
||||
testResult := ModerationTestResult{
|
||||
IsAbnormal: result.Flagged,
|
||||
Details: make([]ModerationTestDetail, 0),
|
||||
}
|
||||
|
||||
// 构建详细信息
|
||||
for category, description := range types.ModerationCategories {
|
||||
score := result.CategoryScores[category]
|
||||
isCategory := result.Categories[category]
|
||||
|
||||
testResult.Details = append(testResult.Details, ModerationTestDetail{
|
||||
Category: category,
|
||||
Description: description,
|
||||
Confidence: fmt.Sprintf("%.2f", score),
|
||||
IsCategory: isCategory,
|
||||
})
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, testResult)
|
||||
}
|
||||
@@ -29,6 +29,14 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
return &OrderHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *OrderHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/order/")
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("clear", h.Clear)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
@@ -68,16 +76,16 @@ func (h *OrderHandler) List(c *gin.Context) {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
payMethod, ok := types.PayMethods[item.PayWay]
|
||||
payChannel, ok := types.PayChannel[item.Channel]
|
||||
if !ok {
|
||||
payMethod = item.PayWay
|
||||
payChannel = item.Channel
|
||||
}
|
||||
payName, ok := types.PayNames[item.PayType]
|
||||
payWays, ok := types.PayWays[item.PayWay]
|
||||
if !ok {
|
||||
payName = item.PayWay
|
||||
payWays = item.PayWay
|
||||
}
|
||||
order.PayMethod = payMethod
|
||||
order.PayName = payName
|
||||
order.ChannelName = payChannel
|
||||
order.PayName = payWays
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
@@ -121,8 +129,8 @@ func (h *OrderHandler) Clear(c *gin.Context) {
|
||||
}
|
||||
deleteIds := make([]uint, 0)
|
||||
for _, order := range orders {
|
||||
// 只删除 15 分钟内的未支付订单
|
||||
if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) {
|
||||
// 只删除超时的未支付订单
|
||||
if time.Now().After(order.CreatedAt.Add(time.Minute * time.Duration(h.App.SysConfig.Base.OrderPayTimeout))) {
|
||||
deleteIds = append(deleteIds, order.Id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
|
||||
return &PowerLogHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *PowerLogHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/powerLog/")
|
||||
group.POST("list", h.List)
|
||||
}
|
||||
|
||||
func (h *PowerLogHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service/ppt"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PPTHandler 管理后台 PPT 生成配置处理器
|
||||
type PPTHandler struct {
|
||||
handler.BaseHandler
|
||||
pptService *ppt.PptService
|
||||
}
|
||||
|
||||
// NewPPTHandler 创建管理后台 PPT 配置处理器
|
||||
func NewPPTHandler(app *core.AppServer, db *gorm.DB, pptService *ppt.PptService) *PPTHandler {
|
||||
return &PPTHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
pptService: pptService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册 PPT 配置相关路由
|
||||
func (h *PPTHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/admin/ppt/")
|
||||
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
rg.GET("config", h.GetConfig)
|
||||
rg.POST("config/update", h.UpdateConfig)
|
||||
rg.GET("jobs", h.Jobs)
|
||||
rg.GET("jobs/:task_id", h.JobDetail)
|
||||
rg.GET("jobs/:task_id/export", h.ExportJob)
|
||||
rg.GET("stats", h.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取 PPT 生成配置
|
||||
func (h *PPTHandler) GetConfig(c *gin.Context) {
|
||||
var cfg model.Config
|
||||
err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 返回一个默认空配置
|
||||
resp.SUCCESS(c, types.PPTConfig{
|
||||
OutlineLLMModel: "gpt-4o-mini",
|
||||
MaxSlidesPerTask: 30,
|
||||
PowerCostPerSlide: 0,
|
||||
MaxConcurrentRequests: 3,
|
||||
QPSLimit: 1,
|
||||
NanoBananaModel: "nano-banana",
|
||||
NanoBananaAspectRatio: "16:9",
|
||||
SeedreamSize: "1920x1080",
|
||||
})
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, "获取配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var pptConfig types.PPTConfig
|
||||
err = utils.JsonDecode(cfg.Value, &pptConfig)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "解析配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, pptConfig)
|
||||
}
|
||||
|
||||
// UpdateConfig 更新 PPT 生成配置
|
||||
func (h *PPTHandler) UpdateConfig(c *gin.Context) {
|
||||
var req types.PPTConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 基础校验
|
||||
if req.MaxSlidesPerTask <= 0 {
|
||||
resp.ERROR(c, "单个任务最多 PPT 页数必须大于 0")
|
||||
return
|
||||
}
|
||||
|
||||
if req.PowerCostPerSlide < 0 {
|
||||
resp.ERROR(c, "每张 PPT 图片消耗算力不能小于 0")
|
||||
return
|
||||
}
|
||||
|
||||
if req.MaxConcurrentRequests <= 0 {
|
||||
req.MaxConcurrentRequests = 3
|
||||
}
|
||||
if req.QPSLimit <= 0 {
|
||||
req.QPSLimit = 1
|
||||
}
|
||||
|
||||
// 根据当前生图提供方做必填校验
|
||||
switch req.ActiveImageProvider {
|
||||
case types.PPTImageProviderNanoBanana:
|
||||
if req.NanoBananaApiURL == "" {
|
||||
resp.ERROR(c, "Nano Banana API 地址不能为空")
|
||||
return
|
||||
}
|
||||
if req.NanoBananaApiKey == "" {
|
||||
resp.ERROR(c, "Nano Banana API Key 不能为空")
|
||||
return
|
||||
}
|
||||
case types.PPTImageProviderSeedream:
|
||||
if req.SeedreamBaseURL == "" {
|
||||
resp.ERROR(c, "Seedream Base URL 不能为空")
|
||||
return
|
||||
}
|
||||
if req.SeedreamApiKey == "" {
|
||||
resp.ERROR(c, "Seedream API Key 不能为空")
|
||||
return
|
||||
}
|
||||
if req.SeedreamModel == "" {
|
||||
resp.ERROR(c, "Seedream 模型 ID 不能为空")
|
||||
return
|
||||
}
|
||||
default:
|
||||
// 允许为空,未来可以扩展更多 provider
|
||||
}
|
||||
|
||||
value := utils.JsonEncode(&req)
|
||||
var cfg model.Config
|
||||
err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
cfg.Name = types.ConfigKeyPPT
|
||||
cfg.Value = value
|
||||
if err = h.DB.Create(&cfg).Error; err != nil {
|
||||
resp.ERROR(c, "创建配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"message": "配置创建成功"})
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, "获取配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Value = value
|
||||
if err = h.DB.Updates(&cfg).Error; err != nil {
|
||||
resp.ERROR(c, "更新配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
|
||||
}
|
||||
|
||||
// Jobs 管理后台查看 PPT 任务列表(内存任务)
|
||||
func (h *PPTHandler) Jobs(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
filterUserId := h.GetInt(c, "user_id", 0)
|
||||
status := h.GetTrim(c, "status")
|
||||
|
||||
filtered, total := h.pptService.ListAdminJobs(c.Request.Context(), page, pageSize, filterUserId, status)
|
||||
|
||||
jobs := make([]gin.H, 0, len(filtered))
|
||||
for _, t := range filtered {
|
||||
job := t.TaskSummaryMap()
|
||||
job["user_id"] = t.UserID
|
||||
job["error_message"] = t.ErrorMessage
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"jobs": jobs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func buildAdminPPTTaskDetail(task *ppt.Task) gin.H {
|
||||
percentage := 0
|
||||
if task.Total > 0 {
|
||||
percentage = int(float64(task.Completed) / float64(task.Total) * 100)
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"task_id": task.TaskID,
|
||||
"user_id": task.UserID,
|
||||
"status": task.Status,
|
||||
"progress": gin.H{
|
||||
"total_slides": task.Total,
|
||||
"completed_slides": task.Completed,
|
||||
"percentage": percentage,
|
||||
},
|
||||
"slides": task.Slides,
|
||||
"error_message": task.ErrorMessage,
|
||||
"content": task.Content,
|
||||
"prompt": task.Prompt,
|
||||
"title": task.Title,
|
||||
"thumb": task.Thumb,
|
||||
}
|
||||
}
|
||||
|
||||
// JobDetail 管理后台查看指定 PPT 任务详情
|
||||
func (h *PPTHandler) JobDetail(c *gin.Context) {
|
||||
taskID := c.Param("task_id")
|
||||
if taskID == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
task, ok := h.pptService.GetTask(taskID)
|
||||
if !ok {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
h.pptService.EnsureTaskMeta(c.Request.Context(), task)
|
||||
resp.SUCCESS(c, buildAdminPPTTaskDetail(task))
|
||||
}
|
||||
|
||||
// ExportJob 管理后台导出 PPT 任务
|
||||
func (h *PPTHandler) ExportJob(c *gin.Context) {
|
||||
taskID := c.Param("task_id")
|
||||
if taskID == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
ef, ok := ppt.ParseExportFormat(c.Query("format"))
|
||||
if !ok {
|
||||
resp.ERROR(c, "format 参数无效,支持 pdf 或 pptx")
|
||||
return
|
||||
}
|
||||
|
||||
task, exists := h.pptService.GetTask(taskID)
|
||||
if !exists {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if task.Status != ppt.TaskStatusCompleted {
|
||||
resp.ERROR(c, "仅已完成任务可导出")
|
||||
return
|
||||
}
|
||||
|
||||
h.pptService.EnsureTaskMeta(c.Request.Context(), task)
|
||||
|
||||
ossCfg := types.OSSConfig{}
|
||||
if h.App.SysConfig != nil {
|
||||
ossCfg = h.App.SysConfig.OSS
|
||||
}
|
||||
data, err := ppt.BuildExportBytes(c.Request.Context(), task.Slides, ef, ossCfg, h.App.Config)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
base := ppt.SanitizeExportBaseName(task.Title, task.TaskID)
|
||||
filename := base + ppt.ExportFileExt(ef)
|
||||
c.Header("Content-Disposition", ppt.ContentDispositionAttachment(filename))
|
||||
c.Data(200, ppt.ExportMimeType(ef), data)
|
||||
}
|
||||
|
||||
// Stats PPT 任务统计信息
|
||||
func (h *PPTHandler) Stats(c *gin.Context) {
|
||||
total, completed, processing, failed, pending := h.pptService.Stats()
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"totalTasks": total,
|
||||
"completedTasks": completed,
|
||||
"processingTasks": processing,
|
||||
"failedTasks": failed,
|
||||
"pendingTasks": pending,
|
||||
})
|
||||
}
|
||||
@@ -15,9 +15,10 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
@@ -28,14 +29,22 @@ func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||
return &ProductHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ProductHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/product/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Days int `json:"days"`
|
||||
Power int `json:"power"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
@@ -45,12 +54,10 @@ func (h *ProductHandler) Save(c *gin.Context) {
|
||||
}
|
||||
|
||||
item := model.Product{
|
||||
Name: data.Name,
|
||||
Price: data.Price,
|
||||
Discount: data.Discount,
|
||||
Days: data.Days,
|
||||
Power: data.Power,
|
||||
Enabled: data.Enabled}
|
||||
Name: data.Name,
|
||||
Price: data.Price,
|
||||
Power: data.Power,
|
||||
Enabled: data.Enabled}
|
||||
item.Id = data.Id
|
||||
if item.Id > 0 {
|
||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
|
||||
@@ -29,6 +29,16 @@ func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler {
|
||||
return &RedeemHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *RedeemHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/redeem/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("create", h.Create)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("export", h.Export)
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SunoHandler struct {
|
||||
handler.BaseHandler
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *SunoHandler {
|
||||
return &SunoHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *SunoHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/suno/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list", h.SunoList)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
type sunoQuery struct {
|
||||
Title string `json:"title"`
|
||||
Prompt string `json:"prompt"`
|
||||
CreatedAt []string `json:"created_at"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// SunoList Suno 任务列表
|
||||
func (h *SunoHandler) SunoList(c *gin.Context) {
|
||||
var data sunoQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Title != "" {
|
||||
session = session.Where("title LIKE ?", "%"+data.Title+"%")
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
// 同时查询 prompt 字段和 params JSON 字段中的 prompt
|
||||
session = session.Where("prompt LIKE ? OR JSON_EXTRACT(params, '$.prompt') LIKE ?", "%"+data.Prompt+"%", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.SunoJob{}).Count(&total)
|
||||
var list []model.SunoJob
|
||||
var items = make([]vo.SunoJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.SunoJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var job model.SunoJob
|
||||
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
md := "suno"
|
||||
power := job.Power
|
||||
userId := int(job.UserId)
|
||||
remark := fmt.Sprintf("SUNO 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
needRefund := job.Progress != 100
|
||||
fileURL := job.AudioURL
|
||||
|
||||
if needRefund {
|
||||
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
// remove file
|
||||
err := h.uploader.GetUploadHandler().Delete(fileURL)
|
||||
if err != nil {
|
||||
logger.Error("remove file failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/handler"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
@@ -28,6 +29,17 @@ func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderMan
|
||||
return &UploadHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, uploaderManager: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *UploadHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/upload")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("", h.Upload)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
// 判断文件大小
|
||||
f, err := c.FormFile("file")
|
||||
@@ -36,7 +48,7 @@ func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.MaxFileSize > 0 && f.Size > int64(h.App.SysConfig.MaxFileSize)*1024*1024 {
|
||||
if h.App.SysConfig.Base.MaxFileSize > 0 && f.Size > int64(h.App.SysConfig.Base.MaxFileSize)*1024*1024 {
|
||||
resp.ERROR(c, "文件大小超过限制")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,32 +8,263 @@ package admin
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/xuri/excelize/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
handler.BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
redis *redis.Client
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService, redisCli *redis.Client) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService, redis: redisCli}
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, redisCli *redis.Client) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, redis: redisCli}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *UserHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/admin/user/")
|
||||
|
||||
// 需要管理员授权的接口
|
||||
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("loginLog", h.LoginLog)
|
||||
group.GET("genLoginLink", h.GenLoginLink)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
group.GET("import/template", h.ImportTemplate)
|
||||
group.POST("import", h.ImportUsers)
|
||||
}
|
||||
}
|
||||
|
||||
// ImportTemplate 下载用户导入模板
|
||||
func (h *UserHandler) ImportTemplate(c *gin.Context) {
|
||||
f := excelize.NewFile()
|
||||
sheetName := "Sheet1"
|
||||
// 表头
|
||||
headers := []string{"用户名", "密码", "手机", "邮箱", "剩余算力", "启用状态"}
|
||||
for i, title := range headers {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||||
_ = f.SetCellValue(sheetName, cell, title)
|
||||
}
|
||||
// 示例数据
|
||||
sample := []interface{}{"user001", "Passw0rd!", "13800000000", "user001@example.com", 100, 1}
|
||||
for i, v := range sample {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 2)
|
||||
_ = f.SetCellValue(sheetName, cell, v)
|
||||
}
|
||||
|
||||
buf, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
logger.Error("failed to generate user import template: ", err)
|
||||
resp.ERROR(c, "生成模板失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
c.Header("Content-Disposition", "attachment; filename=\"user_import_template.xlsx\"")
|
||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes())
|
||||
}
|
||||
|
||||
// ImportUsers 批量导入用户
|
||||
func (h *UserHandler) ImportUsers(c *gin.Context) {
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
resp.ERROR(c, "文件上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
|
||||
if ext != ".xlsx" {
|
||||
resp.ERROR(c, "只支持 .xlsx 格式的 Excel 文件")
|
||||
return
|
||||
}
|
||||
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
resp.ERROR(c, "无法读取上传文件: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 读取到内存,避免多次读取问题
|
||||
var buf bytes.Buffer
|
||||
if _, err = buf.ReadFrom(src); err != nil {
|
||||
resp.ERROR(c, "读取文件内容失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
excel, err := excelize.OpenReader(bytes.NewReader(buf.Bytes()))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "解析 Excel 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = excel.Close()
|
||||
}()
|
||||
|
||||
rows, err := excel.GetRows("Sheet1")
|
||||
if err != nil {
|
||||
resp.ERROR(c, "读取工作表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if len(rows) < 2 {
|
||||
resp.ERROR(c, "Excel 中没有可导入的数据")
|
||||
return
|
||||
}
|
||||
|
||||
type rowError struct {
|
||||
Row int `json:"row"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
var (
|
||||
successCount int
|
||||
failedCount int
|
||||
errorsList []rowError
|
||||
)
|
||||
|
||||
usernameSet := make(map[string]struct{})
|
||||
|
||||
// 从第二行开始读取
|
||||
for index, row := range rows[1:] {
|
||||
line := index + 2 // Excel 行号
|
||||
if len(row) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
get := func(i int) string {
|
||||
if i < len(row) {
|
||||
return strings.TrimSpace(row[i])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
username := get(0)
|
||||
password := get(1)
|
||||
mobile := get(2)
|
||||
email := get(3)
|
||||
powerStr := get(4)
|
||||
statusStr := get(5)
|
||||
|
||||
// 基础校验
|
||||
if username == "" {
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "用户名不能为空"})
|
||||
continue
|
||||
}
|
||||
if _, ok := usernameSet[username]; ok {
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "同一文件中用户名重复"})
|
||||
continue
|
||||
}
|
||||
usernameSet[username] = struct{}{}
|
||||
|
||||
if len(password) < 8 || len(password) > 16 {
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "密码必须为 8-16 位"})
|
||||
continue
|
||||
}
|
||||
|
||||
if mobile != "" && len(mobile) != 11 {
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "手机号必须为 11 位"})
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析算力
|
||||
power := 0
|
||||
if powerStr != "" {
|
||||
p, err := strconv.Atoi(powerStr)
|
||||
if err != nil {
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "剩余算力必须为数字"})
|
||||
continue
|
||||
}
|
||||
if p < 0 {
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "剩余算力不能为负数"})
|
||||
continue
|
||||
}
|
||||
power = p
|
||||
}
|
||||
|
||||
// 解析启用状态
|
||||
status := true
|
||||
if statusStr != "" {
|
||||
switch strings.TrimSpace(statusStr) {
|
||||
case "0", "否", "false", "停用":
|
||||
status = false
|
||||
case "1", "是", "true", "启用":
|
||||
status = true
|
||||
default:
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "启用状态只支持 1/是 或 0/否"})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
var exist model.User
|
||||
if err = h.DB.Where("username = ?", username).First(&exist).Error; err == nil && exist.Id > 0 {
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "用户名已存在"})
|
||||
continue
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
u := model.User{
|
||||
Username: username,
|
||||
Password: utils.GenPassword(password, salt),
|
||||
Mobile: mobile,
|
||||
Email: email,
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Power: power,
|
||||
Status: status,
|
||||
ChatRoles: utils.JsonEncode([]string{}),
|
||||
ChatConfig: "{}",
|
||||
ChatModels: utils.JsonEncode([]int{}),
|
||||
ExpiredTime: 0, // 长期有效
|
||||
Vip: false,
|
||||
}
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
|
||||
if err = h.DB.Create(&u).Error; err != nil {
|
||||
failedCount++
|
||||
errorsList = append(errorsList, rowError{Row: line, Error: "写入数据库失败: " + err.Error()})
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"success": successCount,
|
||||
"failed": failedCount,
|
||||
"errors": errorsList,
|
||||
})
|
||||
}
|
||||
|
||||
// List 用户列表
|
||||
@@ -82,29 +313,21 @@ func (h *UserHandler) List(c *gin.Context) {
|
||||
|
||||
func (h *UserHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
ChatRoles []string `json:"chat_roles"`
|
||||
ChatModels []int `json:"chat_models"`
|
||||
ExpiredTime string `json:"expired_time"`
|
||||
Status bool `json:"status"`
|
||||
Vip bool `json:"vip"`
|
||||
Power int `json:"power"`
|
||||
Id uint `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
ChatModels []int `json:"chat_models"`
|
||||
ExpiredTime string `json:"expired_time"`
|
||||
Status bool `json:"status"`
|
||||
Vip bool `json:"vip"`
|
||||
Power int `json:"power"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
var user = model.User{}
|
||||
var res *gorm.DB
|
||||
var userVo vo.User
|
||||
@@ -121,11 +344,10 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
user.Status = data.Status
|
||||
user.Vip = data.Vip
|
||||
user.Power = data.Power
|
||||
user.ChatRoles = utils.JsonEncode(data.ChatRoles)
|
||||
user.ChatModels = utils.JsonEncode(data.ChatModels)
|
||||
user.ExpiredTime = utils.Str2stamp(data.ExpiredTime)
|
||||
|
||||
res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user)
|
||||
res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_models_json", "expired_time").Updates(&user)
|
||||
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
@@ -177,16 +399,11 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
Salt: salt,
|
||||
Power: data.Power,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode(data.ChatRoles),
|
||||
ChatConfig: "{}",
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
}
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
}
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
res = h.DB.Create(&u)
|
||||
_ = utils.CopyObject(u, &userVo)
|
||||
userVo.Id = u.Id
|
||||
@@ -275,10 +492,7 @@ func (h *UserHandler) Remove(c *gin.Context) {
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.SdJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.DallJob{}).Error; err != nil {
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.ImageJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.SunoJob{}).Error; err != nil {
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// VideoHandler 管理后台视频生成处理器
|
||||
type VideoHandler struct {
|
||||
handler.BaseHandler
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
// NewVideoHandler 创建管理后台视频生成处理器
|
||||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *VideoHandler {
|
||||
return &VideoHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
userService: userService,
|
||||
uploader: manager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册视频生成管理后台路由
|
||||
func (h *VideoHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/admin/video/")
|
||||
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||
{
|
||||
rg.GET("config", h.GetConfig)
|
||||
rg.POST("config/update", h.UpdateConfig)
|
||||
rg.POST("list", h.Videos)
|
||||
rg.GET("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取视频生成配置
|
||||
func (h *VideoHandler) GetConfig(c *gin.Context) {
|
||||
var config model.Config
|
||||
err := h.DB.Where("name", types.ConfigKeyVideo).First(&config).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 返回空配置
|
||||
resp.SUCCESS(c, types.VideoConfig{
|
||||
ApiURL: "",
|
||||
ApiKey: "",
|
||||
VideoPowers: make(map[string]types.VideoModelPower),
|
||||
})
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, "获取配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var videoConfig types.VideoConfig
|
||||
err = utils.JsonDecode(config.Value, &videoConfig)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "解析配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, videoConfig)
|
||||
}
|
||||
|
||||
// UpdateConfig 更新视频生成配置
|
||||
func (h *VideoHandler) UpdateConfig(c *gin.Context) {
|
||||
var req types.VideoConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.ApiURL == "" {
|
||||
resp.ERROR(c, "API地址不能为空")
|
||||
return
|
||||
}
|
||||
if req.ApiKey == "" {
|
||||
resp.ERROR(c, "API密钥不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证算力配置
|
||||
if len(req.VideoPowers) == 0 {
|
||||
resp.ERROR(c, "请至少配置一个模型的算力")
|
||||
return
|
||||
}
|
||||
|
||||
// 新的价格配置方式直接使用 power_config 中的 key(如 "fixed"、"5_720P" 等)
|
||||
// 不再区分固定收费和按秒收费,所有价格配置都在 power_config 中
|
||||
for key, modelPower := range req.VideoPowers {
|
||||
// 验证 provider
|
||||
if modelPower.Provider == "" {
|
||||
resp.ERROR(c, fmt.Sprintf("模型 %s 的 provider 不能为空", key))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 model
|
||||
if modelPower.Model == "" {
|
||||
resp.ERROR(c, fmt.Sprintf("模型 %s 的 model 不能为空", key))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 power_config
|
||||
if len(modelPower.PowerConfig) == 0 {
|
||||
resp.ERROR(c, fmt.Sprintf("模型 %s 的 power_config 不能为空", key))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 power_config 中的值必须大于0
|
||||
for configKey, configValue := range modelPower.PowerConfig {
|
||||
if configValue <= 0 {
|
||||
resp.ERROR(c, fmt.Sprintf("模型 %s 的 power_config.%s 必须大于0", key, configKey))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
tx := h.DB.Begin()
|
||||
value := utils.JsonEncode(&req)
|
||||
var exist model.Config
|
||||
tx.Where("name", types.ConfigKeyVideo).First(&exist)
|
||||
|
||||
if exist.Id > 0 {
|
||||
exist.Value = value
|
||||
err := tx.Updates(&exist).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新配置失败: "+err.Error())
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
exist.Name = types.ConfigKeyVideo
|
||||
exist.Value = value
|
||||
err := tx.Create(&exist).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "创建配置失败: "+err.Error())
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
|
||||
}
|
||||
|
||||
type videoQuery struct {
|
||||
Type string `json:"type"` // 任务类型 luma, keling
|
||||
Status string `json:"status"` // 任务状态 pending, in_progress, downloading, success, failed
|
||||
Prompt string `json:"prompt"`
|
||||
CreatedAt []string `json:"created_at"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// Videos 视频任务列表
|
||||
func (h *VideoHandler) Videos(c *gin.Context) {
|
||||
var data videoQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Type != "" {
|
||||
session = session.Where("type", data.Type)
|
||||
}
|
||||
if data.Status != "" {
|
||||
session = session.Where("status", data.Status)
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.VideoJob{}).Count(&total)
|
||||
var list []model.VideoJob
|
||||
var items = make([]vo.VideoJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.VideoJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
func (h *VideoHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
tab := c.Query("tab")
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var md, remark, fileURL string
|
||||
var power, userId int
|
||||
var needRefund bool
|
||||
|
||||
switch tab {
|
||||
case "luma", "keling":
|
||||
var job model.VideoJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
md = job.Type
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("视频任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
needRefund = job.Status != types.VideoStatusSuccess
|
||||
fileURL = job.VideoURL
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if needRefund {
|
||||
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
// remove file
|
||||
err := h.uploader.GetUploadHandler().Delete(fileURL)
|
||||
if err != nil {
|
||||
logger.Error("remove file failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -12,16 +12,17 @@ import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/log"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
var logger = log.GetLogger()
|
||||
|
||||
type BaseHandler struct {
|
||||
App *core.AppServer
|
||||
@@ -69,6 +70,14 @@ func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
|
||||
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetAdminId(c *gin.Context) uint {
|
||||
userId, ok := c.Get(types.AdminUserID)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) IsLogin(c *gin.Context) bool {
|
||||
return h.GetLoginUserId(c) > 0
|
||||
}
|
||||
|
||||
@@ -8,23 +8,45 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 今日头条函数实现
|
||||
|
||||
type CaptchaHandler struct {
|
||||
App *core.AppServer
|
||||
service *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler {
|
||||
return &CaptchaHandler{service: s}
|
||||
func NewCaptchaHandler(app *core.AppServer, s *service.CaptchaService, sysConfig *types.SystemConfig) *CaptchaHandler {
|
||||
return &CaptchaHandler{App: app, service: s}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *CaptchaHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/captcha/")
|
||||
|
||||
// 无需授权的接口
|
||||
group.GET("get", h.Get)
|
||||
group.POST("check", h.Check)
|
||||
group.GET("slide/get", h.SlideGet)
|
||||
group.POST("slide/check", h.SlideCheck)
|
||||
group.GET("config", h.GetConfig)
|
||||
}
|
||||
|
||||
func (h *CaptchaHandler) GetConfig(c *gin.Context) {
|
||||
resp.SUCCESS(c, gin.H{"enabled": h.service.GetConfig().Enabled, "type": h.service.GetConfig().Type})
|
||||
}
|
||||
|
||||
func (h *CaptchaHandler) Get(c *gin.Context) {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Get()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
@@ -36,6 +58,11 @@ func (h *CaptchaHandler) Get(c *gin.Context) {
|
||||
|
||||
// Check verify the captcha data
|
||||
func (h *CaptchaHandler) Check(c *gin.Context) {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
Dots string `json:"dots"`
|
||||
@@ -55,6 +82,11 @@ func (h *CaptchaHandler) Check(c *gin.Context) {
|
||||
|
||||
// SlideGet 获取滑动验证图片
|
||||
func (h *CaptchaHandler) SlideGet(c *gin.Context) {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.SlideGet()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
@@ -66,6 +98,11 @@ func (h *CaptchaHandler) SlideGet(c *gin.Context) {
|
||||
|
||||
// SlideCheck 滑动验证结果校验
|
||||
func (h *CaptchaHandler) SlideCheck(c *gin.Context) {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
X int `json:"x"`
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatAppHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
|
||||
return &ChatAppHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatAppHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/app/")
|
||||
group.GET("list", h.List)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list/user", h.ListByUser)
|
||||
group.POST("create", h.Create)
|
||||
group.POST("copy", h.Copy)
|
||||
group.POST("update", h.UpdateApp)
|
||||
group.POST("workspace", h.UpdateWorkArea)
|
||||
group.POST("remove", h.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取用户聊天应用列表
|
||||
func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
tid := h.GetInt(c, "tid", 0)
|
||||
var roles []model.ChatApp
|
||||
session := h.DB.Where("enable = ? AND user_id = 0", true)
|
||||
if tid > 0 {
|
||||
session = session.Where("tid", tid)
|
||||
}
|
||||
err := session.Order("sort_num ASC").Find(&roles).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatApp, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatApp
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
if r.UserId == 0 {
|
||||
v.SystemPrompt = ""
|
||||
}
|
||||
roleVos = append(roleVos, v)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, roleVos)
|
||||
}
|
||||
|
||||
// ListByUser 获取用户添加的角色列表
|
||||
func (h *ChatAppHandler) ListByUser(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var roles []model.ChatApp
|
||||
session := h.DB.Where("enable = ?", true)
|
||||
if userId > 0 {
|
||||
session = session.Where("(user_id = 0 OR user_id = ?)", userId)
|
||||
} else {
|
||||
session = session.Where("user_id = 0")
|
||||
}
|
||||
|
||||
if id > 0 {
|
||||
session = session.Or("id", id)
|
||||
}
|
||||
res := session.Order("sort_num ASC").Find(&roles)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatApp, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatApp
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
if r.UserId == 0 {
|
||||
v.SystemPrompt = ""
|
||||
}
|
||||
roleVos = append(roleVos, v)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, roleVos)
|
||||
}
|
||||
|
||||
// Create 用户创建智能体
|
||||
func (h *ChatAppHandler) Create(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
var data vo.ChatApp
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
role := model.ChatApp{
|
||||
Name: data.Name,
|
||||
Tid: data.Tid,
|
||||
UserId: userId,
|
||||
SystemPrompt: data.SystemPrompt,
|
||||
HelloMsg: data.HelloMsg,
|
||||
Icon: data.Icon,
|
||||
Enable: true,
|
||||
SortNum: int(data.SortNum),
|
||||
ModelId: data.ModelId,
|
||||
}
|
||||
if role.Icon == "" {
|
||||
role.Icon = "/images/avatar/gpt.png"
|
||||
}
|
||||
if err := h.DB.Create(&role).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
data.Id = role.Id
|
||||
data.UserId = role.UserId
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// Copy 用户复制智能体(复制为当前用户名下)
|
||||
func (h *ChatAppHandler) Copy(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
SourceId uint `json:"source_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.SourceId == 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var src model.ChatApp
|
||||
if err := h.DB.First(&src, body.SourceId).Error; err != nil {
|
||||
resp.ERROR(c, "智能体不存在")
|
||||
return
|
||||
}
|
||||
role := model.ChatApp{
|
||||
Name: src.Name,
|
||||
Tid: src.Tid,
|
||||
UserId: userId,
|
||||
SystemPrompt: src.SystemPrompt,
|
||||
HelloMsg: src.HelloMsg,
|
||||
Icon: src.Icon,
|
||||
Enable: true,
|
||||
SortNum: src.SortNum,
|
||||
ModelId: src.ModelId,
|
||||
}
|
||||
if err := h.DB.Create(&role).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"id": role.Id})
|
||||
}
|
||||
|
||||
// UpdateApp 更新用户聊天应用(仅允许更新自己创建的)
|
||||
func (h *ChatAppHandler) UpdateApp(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
var data vo.ChatApp
|
||||
if err := c.ShouldBindJSON(&data); err != nil || data.Id == 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var role model.ChatApp
|
||||
if err := h.DB.First(&role, data.Id).Error; err != nil {
|
||||
resp.ERROR(c, "智能体不存在")
|
||||
return
|
||||
}
|
||||
if role.UserId != userId {
|
||||
resp.ERROR(c, "无权限修改该智能体")
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{
|
||||
"name": data.Name,
|
||||
"hello_msg": data.HelloMsg,
|
||||
"icon": data.Icon,
|
||||
"model_id": data.ModelId,
|
||||
"system_prompt": data.SystemPrompt,
|
||||
}
|
||||
if err := h.DB.Model(&role).Updates(updates).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, nil)
|
||||
}
|
||||
|
||||
// UpdateWorkArea 更新用户工作区应用列表(存为应用 id 数组)
|
||||
func (h *ChatAppHandler) UpdateWorkArea(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if err := h.DB.Model(&model.User{}).Where("id = ?", userId).Update("chat_roles_json", utils.JsonEncode(body.Ids)).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, nil)
|
||||
}
|
||||
|
||||
// Remove 删除用户智能体(仅允许删除自己创建的)
|
||||
func (h *ChatAppHandler) Remove(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Id uint `json:"id"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&body)
|
||||
if body.Id == 0 {
|
||||
body.Id = uint(h.GetInt(c, "id", 0))
|
||||
}
|
||||
if body.Id == 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var role model.ChatApp
|
||||
if err := h.DB.First(&role, body.Id).Error; err != nil {
|
||||
resp.ERROR(c, "智能体不存在")
|
||||
return
|
||||
}
|
||||
if role.UserId != userId {
|
||||
resp.ERROR(c, "无权限删除该智能体")
|
||||
return
|
||||
}
|
||||
if err := h.DB.Delete(&role).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, nil)
|
||||
}
|
||||
@@ -19,6 +19,12 @@ func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler
|
||||
return &ChatAppTypeHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatAppTypeHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/app/type/")
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
|
||||
// List 获取App类型列表
|
||||
func (h *ChatAppTypeHandler) List(c *gin.Context) {
|
||||
var items []model.AppType
|
||||
|
||||
+198
-277
@@ -14,9 +14,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sora"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
@@ -39,6 +42,7 @@ import (
|
||||
const (
|
||||
ChatEventStart = "start"
|
||||
ChatEventEnd = "end"
|
||||
ChatEventComplete = "complete"
|
||||
ChatEventError = "error"
|
||||
ChatEventMessageDelta = "message_delta"
|
||||
ChatEventTitle = "title"
|
||||
@@ -54,47 +58,95 @@ type ChatInput struct {
|
||||
Stream bool `json:"stream"`
|
||||
Files []vo.File `json:"files"`
|
||||
ChatModel model.ChatModel `json:"chat_model,omitempty"`
|
||||
ChatRole model.ChatRole `json:"chat_role,omitempty"`
|
||||
ChatRole model.ChatApp `json:"chat_role,omitempty"`
|
||||
LastMsgId uint `json:"last_msg_id,omitempty"` // 最后的消息ID,用于重新生成答案的时候过滤上下文
|
||||
}
|
||||
|
||||
type ChatHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
userService *service.UserService
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
userLocks *types.UserLockManager
|
||||
soraService *sora.SoraService
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService) *ChatHandler {
|
||||
func NewChatHandler(app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
redis *redis.Client,
|
||||
manager *oss.UploaderManager,
|
||||
userService *service.UserService,
|
||||
soraService *sora.SoraService,
|
||||
moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
userService: userService,
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
userLocks: types.NewUserLockManager(),
|
||||
soraService: soraService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/chat/")
|
||||
group.GET("detail", h.Detail)
|
||||
group.GET("history", h.History)
|
||||
// 其他接口需要用户授权
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.Any("message", h.Chat)
|
||||
group.GET("list", h.List)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("clear", h.Clear)
|
||||
group.POST("tokens", h.Tokens)
|
||||
group.GET("stop", h.StopGenerate)
|
||||
group.POST("tts", h.TextToSpeech)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Chat 处理聊天请求
|
||||
func (h *ChatHandler) Chat(c *gin.Context) {
|
||||
var input ChatInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置SSE响应头
|
||||
c.Header("Prompt-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
|
||||
var input ChatInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
pushMessage(c, ChatEventError, types.InvalidArgs)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 用户级并发锁,确保同一用户同时只有一个对话请求
|
||||
if !h.userLocks.TryLock(input.UserId) {
|
||||
pushMessage(c, ChatEventError, "您有一个对话请求正在进行中,请稍后再试或先停止当前生成!")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
defer h.userLocks.Unlock(input.UserId)
|
||||
|
||||
ctx, cancel := context.WithCancel(c.Request.Context())
|
||||
defer cancel()
|
||||
|
||||
// 这里做个全局的异常处理,防止整个请求异常,导致 SSE 连接断开
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Errorf("chat handler error: %v", err)
|
||||
pushMessage(c, ChatEventError, err)
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
// 使用旧的聊天数据覆盖模型和角色ID
|
||||
var chat model.ChatItem
|
||||
h.DB.Where("chat_id", input.ChatId).First(&chat)
|
||||
@@ -104,7 +156,7 @@ func (h *ChatHandler) Chat(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 验证聊天角色
|
||||
var chatRole model.ChatRole
|
||||
var chatRole model.ChatApp
|
||||
err := h.DB.First(&chatRole, input.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
pushMessage(c, ChatEventError, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!")
|
||||
@@ -157,7 +209,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
||||
}
|
||||
|
||||
if userVo.Power < input.ChatModel.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, input.ChatModel.Power)
|
||||
return fmt.Errorf("您的算力不足,请购买算力。")
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
@@ -220,17 +272,24 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
||||
// 加载聊天上下文
|
||||
chatCtx := make([]any, 0)
|
||||
messages := make([]any, 0)
|
||||
if h.App.SysConfig.EnableContext {
|
||||
_ = utils.JsonDecode(input.ChatRole.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
if h.App.SysConfig.Base.EnableContext {
|
||||
_ = utils.JsonDecode(input.ChatRole.SystemPrompt, &messages)
|
||||
if h.App.SysConfig.Base.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
|
||||
if input.LastMsgId > 0 { // 重新生成逻辑
|
||||
if input.LastMsgId > 0 { // 重新生成和编辑逻辑
|
||||
var lastMessage model.ChatMessage
|
||||
err = dbSession.Where("id < ?", input.LastMsgId).Where("type", types.ReplyMsg).Order("id DESC").First(&lastMessage).Error
|
||||
if err != nil {
|
||||
input.LastMsgId = 0
|
||||
} else {
|
||||
input.LastMsgId = lastMessage.Id
|
||||
}
|
||||
dbSession = dbSession.Where("id < ?", input.LastMsgId)
|
||||
// 删除对应的聊天记录
|
||||
h.DB.Debug().Where("chat_id", input.ChatId).Where("id >= ?", input.LastMsgId).Delete(&model.ChatMessage{})
|
||||
h.DB.Debug().Where("chat_id", input.ChatId).Where("id > ?", input.LastMsgId).Delete(&model.ChatMessage{})
|
||||
}
|
||||
err = dbSession.Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages).Error
|
||||
err = dbSession.Limit(h.App.SysConfig.Base.ContextDeep).Order("id DESC").Find(&historyMessages).Error
|
||||
if err == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
@@ -258,7 +317,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
||||
}
|
||||
|
||||
// 上下文的深度超出了模型的最大上下文深度
|
||||
if len(chatCtx) >= h.App.SysConfig.ContextDeep {
|
||||
if len(chatCtx) >= h.App.SysConfig.Base.ContextDeep {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -268,6 +327,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
||||
}
|
||||
reqMgs := make([]any, 0)
|
||||
|
||||
// 添加引导提示词,防止模型生成违规内容
|
||||
if h.App.SysConfig.Moderation.EnableGuide {
|
||||
reqMgs = append(reqMgs, map[string]any{
|
||||
"role": "system",
|
||||
"content": h.App.SysConfig.Moderation.GuidePrompt,
|
||||
})
|
||||
}
|
||||
|
||||
for i := len(chatCtx) - 1; i >= 0; i-- {
|
||||
reqMgs = append(reqMgs, chatCtx[i])
|
||||
}
|
||||
@@ -278,24 +345,22 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
||||
for _, file := range input.Files {
|
||||
logger.Debugf("detected file: %+v", file.URL)
|
||||
// 处理图片
|
||||
if isImageURL(file.URL) {
|
||||
if isImageURL(file.URL) || isVideoURL(file.URL) {
|
||||
imgList = append(imgList, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": file.URL,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
modelValue := input.ChatModel.Value
|
||||
if !(strings.Contains(modelValue, "-all") || strings.HasPrefix(modelValue, "gpt-4-gizmo") || strings.HasPrefix(modelValue, "claude")) {
|
||||
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
continue
|
||||
} else {
|
||||
fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
} else if isTextURL(file.URL) {
|
||||
// 处理文件,提取文件内容
|
||||
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
continue
|
||||
} else {
|
||||
fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
logger.Debugf("fileContents: %s", fileContents)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,16 +376,16 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
||||
}
|
||||
|
||||
if len(imgList) > 0 {
|
||||
imgList = append(imgList, map[string]interface{}{
|
||||
imgList = append(imgList, map[string]any{
|
||||
"type": "text",
|
||||
"text": input.Prompt,
|
||||
})
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
req.Messages = append(reqMgs, map[string]any{
|
||||
"role": "user",
|
||||
"content": imgList,
|
||||
})
|
||||
} else {
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
req.Messages = append(reqMgs, map[string]any{
|
||||
"role": "user",
|
||||
"content": finalPrompt,
|
||||
})
|
||||
@@ -348,23 +413,43 @@ func isImageURL(url string) bool {
|
||||
".svg": true,
|
||||
".ico": true,
|
||||
}
|
||||
return validImageExts[ext]
|
||||
}
|
||||
|
||||
if !validImageExts[ext] {
|
||||
// 判断是个链接是否是文本链接
|
||||
func isTextURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送HEAD请求检查Content-Type
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validTextExts := map[string]bool{
|
||||
".txt": true,
|
||||
".doc": true,
|
||||
".docx": true,
|
||||
".pdf": true,
|
||||
}
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
return validTextExts[ext]
|
||||
}
|
||||
|
||||
// 判断是个链接是否是视频
|
||||
func isVideoURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
return strings.HasPrefix(contentType, "image/")
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validVideoExts := map[string]bool{
|
||||
".mp4": true,
|
||||
".avi": true,
|
||||
".mov": true,
|
||||
".wmv": true,
|
||||
}
|
||||
return validVideoExts[ext]
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
@@ -436,7 +521,7 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input ChatInput, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if input.ChatModel.KeyId > 0 {
|
||||
h.DB.Where("id", input.ChatModel.KeyId).Find(apiKey)
|
||||
h.DB.Where("id", input.ChatModel.KeyId).Where("enabled", true).Find(apiKey)
|
||||
} else { // use the last unused key
|
||||
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
@@ -445,11 +530,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
// ONLY allow apiURL in blank list
|
||||
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Debugf("对话请求消息体:%+v", req)
|
||||
var apiURL string
|
||||
p, _ := url.Parse(apiKey.ApiURL)
|
||||
@@ -507,6 +587,7 @@ func (h *ChatHandler) subUserPower(userVo vo.User, input ChatInput, promptTokens
|
||||
}
|
||||
|
||||
func (h *ChatHandler) saveChatHistory(
|
||||
c *gin.Context,
|
||||
req types.ApiRequest,
|
||||
usage Usage,
|
||||
message types.Message,
|
||||
@@ -515,6 +596,34 @@ func (h *ChatHandler) saveChatHistory(
|
||||
promptCreatedAt time.Time,
|
||||
replyCreatedAt time.Time) {
|
||||
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(usage.Content)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
logger.Debugf("moderationResult: %+v", moderationResult)
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: userVo.Id,
|
||||
Source: types.ModerationSourceChat,
|
||||
Input: usage.Prompt,
|
||||
Output: usage.Content,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
pushMessage(c, ChatEventError, "很抱歉,内容触发敏感词预警,AI 无法回答!!!")
|
||||
// 更新用户算力
|
||||
if input.ChatModel.Power > 0 {
|
||||
h.subUserPower(userVo, input, 0, 0)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// 追加聊天记录
|
||||
// for prompt
|
||||
var promptTokens, replyTokens, totalTokens int
|
||||
@@ -555,6 +664,19 @@ func (h *ChatHandler) saveChatHistory(
|
||||
replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
||||
totalTokens = replyTokens + getTotalTokens(req)
|
||||
}
|
||||
|
||||
// 如果是 Sora 相关模型,则下载视频
|
||||
files := make([]vo.File, 0)
|
||||
if strings.HasPrefix(req.Model, "sora") {
|
||||
video, err := h.soraService.DownloadVideoURL(message.Content)
|
||||
if err != nil {
|
||||
logger.Error("failed to download video: ", err)
|
||||
pushMessage(c, ChatEventError, "视频下载失败:"+err.Error())
|
||||
} else {
|
||||
files = append(files, *video)
|
||||
}
|
||||
}
|
||||
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
@@ -563,7 +685,7 @@ func (h *ChatHandler) saveChatHistory(
|
||||
Icon: input.ChatRole.Icon,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: message.Content,
|
||||
Files: input.Files,
|
||||
Files: files,
|
||||
}),
|
||||
Tokens: replyTokens,
|
||||
TotalTokens: totalTokens,
|
||||
@@ -577,6 +699,23 @@ func (h *ChatHandler) saveChatHistory(
|
||||
logger.Error("failed to save reply history message: ", err)
|
||||
}
|
||||
|
||||
// 发送完整聊天记录给前端
|
||||
var messageVo vo.ChatMessage
|
||||
err = utils.CopyObject(historyReplyMsg, &messageVo)
|
||||
if err == nil {
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(historyReplyMsg.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = historyReplyMsg.Content
|
||||
}
|
||||
content.Files = files
|
||||
messageVo.Content = content
|
||||
messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix()
|
||||
messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix()
|
||||
pushMessage(c, ChatEventComplete, messageVo)
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
if input.ChatModel.Power > 0 {
|
||||
h.subUserPower(userVo, input, promptTokens, replyTokens)
|
||||
@@ -701,221 +840,3 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
logger.Error("写入音频数据到响应失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// // OPenAI 消息发送实现
|
||||
// func (h *ChatHandler) sendOpenAiMessage(
|
||||
// req types.ApiRequest,
|
||||
// userVo vo.User,
|
||||
// ctx context.Context,
|
||||
// session *types.ChatSession,
|
||||
// role model.ChatRole,
|
||||
// prompt string,
|
||||
// c *gin.Context) error {
|
||||
// promptCreatedAt := time.Now() // 记录提问时间
|
||||
// start := time.Now()
|
||||
// var apiKey = model.ApiKey{}
|
||||
// response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
// logger.Info("HTTP请求完成,耗时:", time.Since(start))
|
||||
// if err != nil {
|
||||
// if strings.Contains(err.Error(), "context canceled") {
|
||||
// return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
// } else if strings.Contains(err.Error(), "no available key") {
|
||||
// return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
// }
|
||||
// return err
|
||||
// } else {
|
||||
// defer response.Body.Close()
|
||||
// }
|
||||
|
||||
// if response.StatusCode != 200 {
|
||||
// body, _ := io.ReadAll(response.Body)
|
||||
// return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
// contentType := response.Header.Get("Prompt-Type")
|
||||
// if strings.Contains(contentType, "text/event-stream") {
|
||||
// replyCreatedAt := time.Now() // 记录回复时间
|
||||
// // 循环读取 Chunk 消息
|
||||
// var message = types.Message{Role: "assistant"}
|
||||
// var contents = make([]string, 0)
|
||||
// var function model.Function
|
||||
// var toolCall = false
|
||||
// var arguments = make([]string, 0)
|
||||
// var reasoning = false
|
||||
|
||||
// pushMessage(c, ChatEventStart, "开始响应")
|
||||
// scanner := bufio.NewScanner(response.Body)
|
||||
// for scanner.Scan() {
|
||||
// line := scanner.Text()
|
||||
// if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||
// continue
|
||||
// }
|
||||
// var responseBody = types.ApiResponse{}
|
||||
// err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||
// if err != nil { // 数据解析出错
|
||||
// return errors.New(line)
|
||||
// }
|
||||
// if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
|
||||
// continue
|
||||
// }
|
||||
// if responseBody.Choices[0].Delta.Prompt == nil &&
|
||||
// responseBody.Choices[0].Delta.ToolCalls == nil &&
|
||||
// responseBody.Choices[0].Delta.ReasoningContent == "" {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
// pushMessage(c, ChatEventError, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
// break
|
||||
// }
|
||||
|
||||
// var tool types.ToolCall
|
||||
// if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
|
||||
// tool = responseBody.Choices[0].Delta.ToolCalls[0]
|
||||
// if toolCall && tool.Function.Name == "" {
|
||||
// arguments = append(arguments, tool.Function.Arguments)
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 兼容 Function Call
|
||||
// fun := responseBody.Choices[0].Delta.FunctionCall
|
||||
// if fun.Name != "" {
|
||||
// tool = *new(types.ToolCall)
|
||||
// tool.Function.Name = fun.Name
|
||||
// } else if toolCall {
|
||||
// arguments = append(arguments, fun.Arguments)
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if !utils.IsEmptyValue(tool) {
|
||||
// res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
|
||||
// if res.Error == nil {
|
||||
// toolCall = true
|
||||
// callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": callMsg,
|
||||
// })
|
||||
// contents = append(contents, callMsg)
|
||||
// }
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if responseBody.Choices[0].FinishReason == "tool_calls" ||
|
||||
// responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
||||
// break
|
||||
// }
|
||||
|
||||
// // output stopped
|
||||
// if responseBody.Choices[0].FinishReason != "" {
|
||||
// break // 输出完成或者输出中断了
|
||||
// } else { // 正常输出结果
|
||||
// // 兼容思考过程
|
||||
// if responseBody.Choices[0].Delta.ReasoningContent != "" {
|
||||
// reasoningContent := responseBody.Choices[0].Delta.ReasoningContent
|
||||
// if !reasoning {
|
||||
// reasoningContent = fmt.Sprintf("<think>%s", reasoningContent)
|
||||
// reasoning = true
|
||||
// }
|
||||
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": reasoningContent,
|
||||
// })
|
||||
// contents = append(contents, reasoningContent)
|
||||
// } else if responseBody.Choices[0].Delta.Prompt != "" {
|
||||
// finalContent := responseBody.Choices[0].Delta.Prompt
|
||||
// if reasoning {
|
||||
// finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Prompt)
|
||||
// reasoning = false
|
||||
// }
|
||||
// contents = append(contents, utils.InterfaceToString(finalContent))
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": finalContent,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// } // end for
|
||||
|
||||
// if err := scanner.Err(); err != nil {
|
||||
// if strings.Contains(err.Error(), "context canceled") {
|
||||
// logger.Info("用户取消了请求:", prompt)
|
||||
// } else {
|
||||
// logger.Error("信息读取出错:", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// if toolCall { // 调用函数完成任务
|
||||
// params := make(map[string]any)
|
||||
// _ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
||||
// logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
|
||||
// params["user_id"] = userVo.Id
|
||||
// var apiRes types.BizVo
|
||||
// r, err := req2.C().R().SetHeader("Body-Type", "application/json").
|
||||
// SetHeader("Authorization", function.Token).
|
||||
// SetBody(params).Post(function.Action)
|
||||
// errMsg := ""
|
||||
// if err != nil {
|
||||
// errMsg = err.Error()
|
||||
// } else {
|
||||
// all, _ := io.ReadAll(r.Body)
|
||||
// err = json.Unmarshal(all, &apiRes)
|
||||
// if err != nil {
|
||||
// errMsg = err.Error()
|
||||
// } else if apiRes.Code != types.Success {
|
||||
// errMsg = apiRes.Message
|
||||
// }
|
||||
// }
|
||||
|
||||
// if errMsg != "" {
|
||||
// errMsg = "调用函数工具出错:" + errMsg
|
||||
// contents = append(contents, errMsg)
|
||||
// } else {
|
||||
// errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
// contents = append(contents, errMsg)
|
||||
// }
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": errMsg,
|
||||
// })
|
||||
// }
|
||||
|
||||
// // 消息发送成功
|
||||
// if len(contents) > 0 {
|
||||
// usage := Usage{
|
||||
// Prompt: prompt,
|
||||
// Prompt: strings.Join(contents, ""),
|
||||
// PromptTokens: 0,
|
||||
// CompletionTokens: 0,
|
||||
// TotalTokens: 0,
|
||||
// }
|
||||
// message.Prompt = usage.Prompt
|
||||
// h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
// }
|
||||
// } else {
|
||||
// var respVo OpenAIResVo
|
||||
// body, err := io.ReadAll(response.Body)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("读取响应失败:%v", body)
|
||||
// }
|
||||
// err = json.Unmarshal(body, &respVo)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("解析响应失败:%v", body)
|
||||
// }
|
||||
// content := respVo.Choices[0].Message.Prompt
|
||||
// if strings.HasPrefix(req.Model, "o1-") {
|
||||
// content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Prompt)
|
||||
// }
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": content,
|
||||
// })
|
||||
// respVo.Usage.Prompt = prompt
|
||||
// respVo.Usage.Prompt = content
|
||||
// h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
// List 获取会话列表
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
logger.Info(h.GetLoginUserId(c))
|
||||
if !h.IsLogin(c) {
|
||||
resp.SUCCESS(c)
|
||||
return
|
||||
@@ -28,7 +29,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
var items = make([]vo.ChatItem, 0)
|
||||
var chats []model.ChatItem
|
||||
h.DB.Where("user_id", userId).Order("id DESC").Find(&chats)
|
||||
h.DB.Debug().Where("user_id", userId).Order("id DESC").Find(&chats)
|
||||
if len(chats) == 0 {
|
||||
resp.SUCCESS(c, items)
|
||||
return
|
||||
@@ -41,9 +42,9 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
modelValues = append(modelValues, chat.Model)
|
||||
}
|
||||
|
||||
var roles []model.ChatRole
|
||||
var roles []model.ChatApp
|
||||
var models []model.ChatModel
|
||||
roleMap := make(map[uint]model.ChatRole)
|
||||
roleMap := make(map[uint]model.ChatApp)
|
||||
modelMap := make(map[string]model.ChatModel)
|
||||
h.DB.Where("id IN ?", roleIds).Find(&roles)
|
||||
h.DB.Where("value IN ?", modelValues).Find(&models)
|
||||
@@ -204,7 +205,7 @@ func (h *ChatHandler) Detail(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 填充角色名称
|
||||
var role model.ChatRole
|
||||
var role model.ChatApp
|
||||
res = h.DB.Where("id", chatItem.RoleId).First(&role)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Role not found")
|
||||
|
||||
@@ -26,6 +26,12 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||
return &ChatModelHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatModelHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/model/")
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
var items []model.ChatModel
|
||||
|
||||
@@ -212,6 +212,14 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
} else {
|
||||
errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
contents = append(contents, errMsg)
|
||||
// 扣减算力
|
||||
if function.Power > 0 {
|
||||
h.userService.DecreasePower(userVo.Id, function.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: function.Name,
|
||||
Remark: fmt.Sprintf("函数调用,%s(%s)", function.Label, function.Name),
|
||||
})
|
||||
}
|
||||
}
|
||||
pushMessage(c, "text", errMsg)
|
||||
}
|
||||
@@ -226,7 +234,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
TotalTokens: 0,
|
||||
}
|
||||
message.Content = usage.Content
|
||||
h.saveChatHistory(req, usage, message, input, userVo, promptCreatedAt, replyCreatedAt)
|
||||
h.saveChatHistory(c, req, usage, message, input, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else { // 非流式输出
|
||||
var respVo OpenAIResVo
|
||||
@@ -242,7 +250,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
pushMessage(c, "text", content)
|
||||
respVo.Usage.Prompt = input.Prompt
|
||||
respVo.Usage.Content = content
|
||||
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, input, userVo, promptCreatedAt, time.Now())
|
||||
h.saveChatHistory(c, req, respVo.Usage, respVo.Choices[0].Message, input, userVo, promptCreatedAt, time.Now())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatRoleHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
||||
return &ChatRoleHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// List 获取用户聊天应用列表
|
||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
tid := h.GetInt(c, "tid", 0)
|
||||
var roles []model.ChatRole
|
||||
session := h.DB.Where("enable", true)
|
||||
if tid > 0 {
|
||||
session = session.Where("tid", tid)
|
||||
}
|
||||
err := session.Order("sort_num ASC").Find(&roles).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatRole
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
roleVos = append(roleVos, v)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, roleVos)
|
||||
}
|
||||
|
||||
// ListByUser 获取用户添加的角色列表
|
||||
func (h *ChatRoleHandler) ListByUser(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var roles []model.ChatRole
|
||||
session := h.DB.Where("enable", true)
|
||||
// 如果用户没登录,则获取所有角色
|
||||
if userId > 0 {
|
||||
var user model.User
|
||||
h.DB.First(&user, userId)
|
||||
var roleKeys []string
|
||||
if user.ChatRoles != "" {
|
||||
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "角色解析失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
// 保证用户至少有一个角色可用
|
||||
if len(roleKeys) > 0 {
|
||||
session = session.Where("marker IN ?", roleKeys)
|
||||
}
|
||||
}
|
||||
|
||||
if id > 0 {
|
||||
session = session.Or("id", id)
|
||||
}
|
||||
res := session.Order("sort_num ASC").Find(&roles)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatRole
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
roleVos = append(roleVos, v)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, roleVos)
|
||||
}
|
||||
|
||||
// UpdateRole 更新用户聊天角色
|
||||
func (h *ChatRoleHandler) UpdateRole(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Keys []string `json:"keys"`
|
||||
}
|
||||
if err = c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -8,8 +8,10 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"geekai/core"
|
||||
"geekai/service"
|
||||
"geekai/core/types"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
@@ -20,35 +22,62 @@ import (
|
||||
|
||||
type ConfigHandler struct {
|
||||
BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
uploaderManager *oss.UploaderManager
|
||||
sysConfig *types.SystemConfig
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler {
|
||||
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService}
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, uploaderManager *oss.UploaderManager, sysConfig *types.SystemConfig) *ConfigHandler {
|
||||
return &ConfigHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
uploaderManager: uploaderManager,
|
||||
sysConfig: sysConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ConfigHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/config/")
|
||||
|
||||
// 无需授权的接口
|
||||
group.GET("get", h.Get)
|
||||
group.GET("oss/thumb", h.GetOssThumbTemplate)
|
||||
}
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
err := h.DB.Where("name", key).First(&config).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
resp.SUCCESS(c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]interface{}
|
||||
err := utils.JsonDecode(config.Config, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]any
|
||||
err = utils.JsonDecode(config.Value, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if key == types.ConfigKeyWxGzh {
|
||||
delete(value, "secret")
|
||||
delete(value, "token")
|
||||
delete(value, "encoding_aes_key")
|
||||
}
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// License 获取 License 配置
|
||||
func (h *ConfigHandler) License(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license.Configs)
|
||||
// GetOssThumbTemplate 获取当前存储引擎的缩略图模板
|
||||
func (h *ConfigHandler) GetOssThumbTemplate(c *gin.Context) {
|
||||
template := h.uploaderManager.GetThumbTemplate()
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"template": template,
|
||||
"active": h.sysConfig.OSS.Active,
|
||||
})
|
||||
}
|
||||
|
||||
+45
-115
@@ -13,8 +13,7 @@ import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/crawler"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/image"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
@@ -31,9 +30,8 @@ import (
|
||||
|
||||
type FunctionHandler struct {
|
||||
BaseHandler
|
||||
config types.ApiConfig
|
||||
uploadManager *oss.UploaderManager
|
||||
dallService *dalle.Service
|
||||
imageService *image.Service
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
@@ -42,20 +40,30 @@ func NewFunctionHandler(
|
||||
db *gorm.DB,
|
||||
config *types.AppConfig,
|
||||
manager *oss.UploaderManager,
|
||||
dallService *dalle.Service,
|
||||
imageService *image.Service,
|
||||
userService *service.UserService) *FunctionHandler {
|
||||
return &FunctionHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
DB: db,
|
||||
},
|
||||
config: config.ApiConfig,
|
||||
uploadManager: manager,
|
||||
dallService: dallService,
|
||||
imageService: imageService,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *FunctionHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/function/")
|
||||
group.GET("list", h.List)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.POST("weibo", h.WeiBo)
|
||||
group.POST("zaobao", h.ZaoBao)
|
||||
group.POST("image3", h.Image3)
|
||||
}
|
||||
|
||||
type resVo struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@@ -107,16 +115,10 @@ func (h *FunctionHandler) WeiBo(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.config.Token == "" {
|
||||
resp.ERROR(c, "无效的 API Token")
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/weibo/fetch", h.config.ApiURL)
|
||||
url := fmt.Sprintf("%s/api/weibo/fetch", types.GeekAPIURL)
|
||||
var res resVo
|
||||
r, err := req.C().R().
|
||||
SetHeader("AppId", h.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
|
||||
SetHeader("Authorization", "Bearer geekai-plus").
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("%v", err))
|
||||
@@ -146,16 +148,10 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.config.Token == "" {
|
||||
resp.ERROR(c, "无效的 API Token")
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/zaobao/fetch", h.config.ApiURL)
|
||||
url := fmt.Sprintf("%s/api/zaobao/fetch", types.GeekAPIURL)
|
||||
var res resVo
|
||||
r, err := req.C().R().
|
||||
SetHeader("AppId", h.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
|
||||
SetHeader("Authorization", "Bearer geekai-plus").
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("%v", err))
|
||||
@@ -180,8 +176,8 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
|
||||
resp.SUCCESS(c, strings.Join(builder, "\n\n"))
|
||||
}
|
||||
|
||||
// Dall3 DallE3 AI 绘图
|
||||
func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
// Image3 AI 图像生成
|
||||
func (h *FunctionHandler) Image3(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -193,47 +189,51 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var chatModel model.ChatModel
|
||||
res := h.DB.Where("type = ?", "img").Where("enabled", true).First(&chatModel)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "没有找到可用的AI绘图模型!")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("绘画参数:%+v", params)
|
||||
var user model.User
|
||||
res := h.DB.Where("id = ?", params["user_id"]).First(&user)
|
||||
res = h.DB.Where("id = ?", params["user_id"]).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "当前用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.DallPower {
|
||||
resp.ERROR(c, "创建 DALL-E 绘图任务失败,算力不足")
|
||||
if user.Power < chatModel.Power {
|
||||
resp.ERROR(c, "创建绘图任务失败,算力不足")
|
||||
return
|
||||
}
|
||||
|
||||
// create dall task
|
||||
// create image task
|
||||
prompt := utils.InterfaceToString(params["prompt"])
|
||||
task := types.DallTask{
|
||||
task := types.ImageTask{
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
ModelId: 0,
|
||||
ModelName: "dall-e-3",
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
N: 1,
|
||||
Quality: "standard",
|
||||
Size: "1024x1024",
|
||||
Style: "vivid",
|
||||
Power: h.App.SysConfig.DallPower,
|
||||
ModelId: chatModel.Id,
|
||||
ModelValue: chatModel.Value,
|
||||
ModelName: chatModel.Name,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
job := model.DallJob{
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
Power: h.App.SysConfig.DallPower,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
job := model.ImageJob{
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
Power: chatModel.Power,
|
||||
Params: utils.JsonEncode(task),
|
||||
}
|
||||
err := h.DB.Create(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+err.Error())
|
||||
resp.ERROR(c, "创建绘图任务失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task.Id = job.Id
|
||||
content, err := h.dallService.Image(task, true)
|
||||
content, err := h.imageService.Image(task, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务执行失败:"+err.Error())
|
||||
return
|
||||
@@ -253,76 +253,6 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
// 实现一个联网搜索的函数工具,采用爬虫实现
|
||||
func (h *FunctionHandler) WebSearch(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var params map[string]interface{}
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 从参数中获取搜索关键词
|
||||
keyword, ok := params["keyword"].(string)
|
||||
if !ok || keyword == "" {
|
||||
resp.ERROR(c, "搜索关键词不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 从参数中获取最大页数,默认为1页
|
||||
maxPages := 1
|
||||
if pages, ok := params["max_pages"].(float64); ok {
|
||||
maxPages = int(pages)
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
userID, ok := params["user_id"].(float64)
|
||||
if !ok {
|
||||
resp.ERROR(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
var user model.User
|
||||
res := h.DB.Where("id = ?", int(userID)).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户算力是否足够
|
||||
searchPower := 1 // 每次搜索消耗1点算力
|
||||
if user.Power < searchPower {
|
||||
resp.ERROR(c, "算力不足,无法执行网络搜索")
|
||||
return
|
||||
}
|
||||
|
||||
// 执行网络搜索
|
||||
searchResults, err := crawler.SearchWeb(keyword, maxPages)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("搜索失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
err = h.userService.DecreasePower(user.Id, searchPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "web_search",
|
||||
Remark: fmt.Sprintf("网络搜索:%s", utils.CutWords(keyword, 10)),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "扣减算力失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回搜索结果
|
||||
resp.SUCCESS(c, searchResults)
|
||||
}
|
||||
|
||||
// List 获取所有的工具函数列表
|
||||
func (h *FunctionHandler) List(c *gin.Context) {
|
||||
var items []model.Function
|
||||
|
||||
@@ -3,16 +3,18 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * that can be found in LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/image"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
@@ -23,18 +25,20 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DallJobHandler struct {
|
||||
type ImageJobHandler struct {
|
||||
BaseHandler
|
||||
dallService *dalle.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
imageService *image.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService) *DallJobHandler {
|
||||
return &DallJobHandler{
|
||||
dallService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
func NewImageJobHandler(app *core.AppServer, db *gorm.DB, service *image.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *ImageJobHandler {
|
||||
return &ImageJobHandler{
|
||||
imageService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
@@ -42,14 +46,55 @@ func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ImageJobHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/image/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("models", h.GetModels)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
var data types.DallTask
|
||||
func (h *ImageJobHandler) Image(c *gin.Context) {
|
||||
var data types.ImageTask
|
||||
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceImage,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,提示词未通过文本审核,请重新输入!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var chatModel model.ChatModel
|
||||
if res := h.DB.Where("id = ?", data.ModelId).First(&chatModel); res.Error != nil {
|
||||
resp.ERROR(c, "模型不存在")
|
||||
@@ -69,22 +114,23 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
task := types.DallTask{
|
||||
task := types.ImageTask{
|
||||
UserId: uint(userId),
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Value,
|
||||
ModelName: chatModel.Name,
|
||||
ModelValue: chatModel.Value,
|
||||
Image: data.Image,
|
||||
Prompt: data.Prompt,
|
||||
Quality: data.Quality,
|
||||
AspectRatio: data.AspectRatio,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
job := model.DallJob{
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Power: chatModel.Power,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
job := model.ImageJob{
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Power: chatModel.Power,
|
||||
Params: utils.JsonEncode(task),
|
||||
}
|
||||
res := h.DB.Create(&job)
|
||||
if res.Error != nil {
|
||||
@@ -93,12 +139,12 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
}
|
||||
|
||||
task.Id = job.Id
|
||||
h.dallService.PushTask(task)
|
||||
h.imageService.PushTask(task)
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Model: chatModel.Name,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -109,7 +155,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ImgWall 照片墙
|
||||
func (h *DallJobHandler) ImgWall(c *gin.Context) {
|
||||
func (h *ImageJobHandler) ImgWall(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
err, jobs := h.getData(true, 0, page, pageSize, true)
|
||||
@@ -121,8 +167,8 @@ func (h *DallJobHandler) ImgWall(c *gin.Context) {
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 SD 任务列表
|
||||
func (h *DallJobHandler) JobList(c *gin.Context) {
|
||||
// JobList 获取 Image 任务列表
|
||||
func (h *ImageJobHandler) JobList(c *gin.Context) {
|
||||
finish := h.GetBool(c, "finish")
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
@@ -139,7 +185,7 @@ func (h *DallJobHandler) JobList(c *gin.Context) {
|
||||
}
|
||||
|
||||
// JobList 获取任务列表
|
||||
func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
|
||||
func (h *ImageJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
@@ -159,21 +205,22 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
|
||||
}
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.DallJob{}).Count(&total)
|
||||
session.Model(&model.ImageJob{}).Count(&total)
|
||||
|
||||
var items []model.DallJob
|
||||
var items []model.ImageJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, vo.Page{}
|
||||
}
|
||||
|
||||
var jobs = make([]vo.DallJob, 0)
|
||||
var jobs = make([]vo.ImageJob, 0)
|
||||
for _, item := range items {
|
||||
var job vo.DallJob
|
||||
var job vo.ImageJob
|
||||
err := utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
@@ -181,10 +228,10 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *DallJobHandler) Remove(c *gin.Context) {
|
||||
func (h *ImageJobHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.DallJob
|
||||
var job model.ImageJob
|
||||
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
@@ -207,12 +254,12 @@ func (h *DallJobHandler) Remove(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Publish 发布/取消发布图片到画廊显示
|
||||
func (h *DallJobHandler) Publish(c *gin.Context) {
|
||||
func (h *ImageJobHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
|
||||
err := h.DB.Model(&model.DallJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
|
||||
err := h.DB.Model(&model.ImageJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -221,7 +268,7 @@ func (h *DallJobHandler) Publish(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *DallJobHandler) GetModels(c *gin.Context) {
|
||||
func (h *ImageJobHandler) GetModels(c *gin.Context) {
|
||||
var models []model.ChatModel
|
||||
err := h.DB.Where("type", "img").Where("enabled", true).Find(&models).Error
|
||||
if err != nil {
|
||||
+134
-14
@@ -8,14 +8,18 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InviteHandler 用户邀请
|
||||
@@ -27,6 +31,23 @@ func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
|
||||
return &InviteHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *InviteHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/invite/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("hits", h.Hits)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("code", h.Code)
|
||||
group.GET("list", h.List)
|
||||
group.GET("stats", h.Stats)
|
||||
group.GET("rules", h.Rules)
|
||||
}
|
||||
}
|
||||
|
||||
// Code 获取当前用户邀请码
|
||||
func (h *InviteHandler) Code(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
@@ -65,21 +86,34 @@ func (h *InviteHandler) List(c *gin.Context) {
|
||||
var total int64
|
||||
session.Model(&model.InviteLog{}).Count(&total)
|
||||
var items []model.InviteLog
|
||||
var list = make([]vo.InviteLog, 0)
|
||||
offset := (page - 1) * pageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var v vo.InviteLog
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err == nil {
|
||||
v.Id = item.Id
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
list = append(list, v)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
err := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userIds := make([]uint, 0)
|
||||
for _, item := range items {
|
||||
userIds = append(userIds, item.UserId)
|
||||
}
|
||||
userMap := make(map[uint]model.User)
|
||||
var users []model.User
|
||||
h.DB.Model(&model.User{}).Where("id IN (?)", userIds).Find(&users)
|
||||
for _, user := range users {
|
||||
userMap[user.Id] = user
|
||||
}
|
||||
|
||||
var list = make([]vo.InviteLog, 0)
|
||||
for _, item := range items {
|
||||
var v vo.InviteLog
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.Avatar = userMap[item.UserId].Avatar
|
||||
list = append(list, v)
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
|
||||
}
|
||||
@@ -90,3 +124,89 @@ func (h *InviteHandler) Hits(c *gin.Context) {
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Stats 获取邀请统计
|
||||
func (h *InviteHandler) Stats(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
// 获取邀请码
|
||||
var inviteCode model.InviteCode
|
||||
res := h.DB.Where("user_id = ?", userId).First(&inviteCode)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "邀请码不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 统计累计邀请数
|
||||
var totalInvite int64
|
||||
h.DB.Model(&model.InviteLog{}).Where("inviter_id = ?", userId).Count(&totalInvite)
|
||||
|
||||
// 统计今日邀请数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var todayInvite int64
|
||||
h.DB.Model(&model.InviteLog{}).Where("inviter_id = ? AND DATE(created_at) = ?", userId, today).Count(&todayInvite)
|
||||
|
||||
// 获取系统配置中的邀请奖励
|
||||
var config model.Config
|
||||
var invitePower int = 200 // 默认值
|
||||
if h.DB.Where("name = ?", "system").First(&config).Error == nil {
|
||||
var configMap map[string]any
|
||||
if utils.JsonDecode(config.Value, &configMap) == nil {
|
||||
if power, ok := configMap["invite_power"].(float64); ok {
|
||||
invitePower = int(power)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算获得奖励总数
|
||||
rewardTotal := int(totalInvite) * invitePower
|
||||
|
||||
// 构建邀请链接
|
||||
inviteLink := fmt.Sprintf("%s/register?invite=%s", h.App.Config.StaticUrl, inviteCode.Code)
|
||||
|
||||
stats := vo.InviteStats{
|
||||
InviteCount: int(totalInvite),
|
||||
RewardTotal: rewardTotal,
|
||||
TodayInvite: int(todayInvite),
|
||||
InviteCode: inviteCode.Code,
|
||||
InviteLink: inviteLink,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, stats)
|
||||
}
|
||||
|
||||
// Rules 获取奖励规则
|
||||
func (h *InviteHandler) Rules(c *gin.Context) {
|
||||
// 获取系统配置中的邀请奖励
|
||||
var config model.Config
|
||||
var invitePower int = 200 // 默认值
|
||||
if h.DB.Where("name = ?", "system").First(&config).Error == nil {
|
||||
var configMap map[string]interface{}
|
||||
if utils.JsonDecode(config.Value, &configMap) == nil {
|
||||
if power, ok := configMap["invite_power"].(float64); ok {
|
||||
invitePower = int(power)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rules := []vo.RewardRule{
|
||||
{
|
||||
Id: 1,
|
||||
Title: "好友注册",
|
||||
Desc: "好友通过邀请链接成功注册",
|
||||
Icon: "icon-user-fill",
|
||||
Color: "#1989fa",
|
||||
Reward: invitePower,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Title: "好友首次充值",
|
||||
Desc: "好友首次充值任意金额",
|
||||
Icon: "icon-money",
|
||||
Color: "#07c160",
|
||||
Reward: invitePower * 2, // 假设首次充值奖励是注册奖励的2倍
|
||||
},
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, rules)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/moderation"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// JimengHandler 即梦AI处理器
|
||||
type JimengHandler struct {
|
||||
BaseHandler
|
||||
jimengService *jimeng.Service
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
// NewJimengHandler 创建即梦AI处理器
|
||||
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *gorm.DB, userService *service.UserService, moderationManager *moderation.ServiceManager) *JimengHandler {
|
||||
return &JimengHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
jimengService: jimengService,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由,新增统一任务接口
|
||||
func (h *JimengHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/jimeng/")
|
||||
group.GET("power-config", h.GetPowerConfig)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("task", h.CreateTask)
|
||||
group.POST("jobs", h.Jobs)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("retry", h.Retry)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTask 统一任务创建接口
|
||||
func (h *JimengHandler) CreateTask(c *gin.Context) {
|
||||
var req types.JimengTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable && req.Prompt != "" {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceJiMeng,
|
||||
Input: req.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if req.Prompt == "" && len(req.ImageUrls) == 0 {
|
||||
resp.ERROR(c, "提示词和图片不能同时为空")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取算力消耗
|
||||
powerCost, err := h.getTaskPower(req)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "计算任务消耗积分失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
req.Power = powerCost
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, &req)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: job.ReqKey,
|
||||
Remark: h.getTaskRemark(req, job.Id),
|
||||
})
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *JimengHandler) getTaskRemark(req types.JimengTaskRequest, jobId uint) string {
|
||||
remark := fmt.Sprintf("即梦任务%s,任务ID:%d", req.ReqKey, jobId)
|
||||
perUnit, ok := h.App.SysConfig.Jimeng.Powers[req.ReqKey]
|
||||
if !ok || perUnit <= 0 {
|
||||
return remark // Fallback if power not found or invalid
|
||||
}
|
||||
switch req.TaskType {
|
||||
case types.JMTaskTypeImage:
|
||||
remark = fmt.Sprintf("即梦图片生成,任务ID:%d,%d积分/张", jobId, perUnit)
|
||||
case types.JMTaskTypeVideo:
|
||||
seconds := 0
|
||||
if perUnit > 0 {
|
||||
seconds = req.Power / perUnit
|
||||
}
|
||||
remark = fmt.Sprintf("即梦视频生成,任务ID:%d,%d积分/秒, %d秒", jobId, perUnit, seconds)
|
||||
case types.JMTaskTypeVirtualHuman:
|
||||
seconds := 0
|
||||
if perUnit > 0 {
|
||||
seconds = req.Power / perUnit
|
||||
}
|
||||
remark = fmt.Sprintf("即梦数字人视频生成,任务ID:%d,%d积分/秒, %d秒", jobId, perUnit, seconds)
|
||||
case types.JMTaskTypeActionTransfer:
|
||||
seconds := 0
|
||||
if perUnit > 0 {
|
||||
seconds = req.Power / perUnit
|
||||
}
|
||||
remark = fmt.Sprintf("即梦视频动作迁移,任务ID:%d,%d积分/秒, %d秒", jobId, perUnit, seconds)
|
||||
}
|
||||
return remark
|
||||
}
|
||||
|
||||
// Jobs 获取任务列表
|
||||
func (h *JimengHandler) Jobs(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
var req struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Filter string `json:"filter"`
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var jobs []model.JimengJob
|
||||
var total int64
|
||||
query := h.DB.Model(&model.JimengJob{}).Where("user_id = ?", userId)
|
||||
|
||||
switch req.Filter {
|
||||
case "image":
|
||||
query = query.Where("type = ?", types.JMTaskTypeImage)
|
||||
case "video":
|
||||
query = query.Where("type = ?", types.JMTaskTypeVideo)
|
||||
case "virtual_human":
|
||||
query = query.Where("type = ?", types.JMTaskTypeVirtualHuman)
|
||||
case "action_transfer":
|
||||
query = query.Where("type = ?", types.JMTaskTypeActionTransfer)
|
||||
}
|
||||
|
||||
if len(req.Ids) > 0 {
|
||||
query = query.Where("id IN (?)", req.Ids)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("updated_at DESC").Offset(offset).Limit(req.PageSize).Find(&jobs).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 填充 VO
|
||||
var jobVos []vo.JimengJob
|
||||
for _, job := range jobs {
|
||||
var jobVo vo.JimengJob
|
||||
err := utils.CopyObject(job, &jobVo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jobVo.CreatedAt = job.CreatedAt.Unix()
|
||||
jobVos = append(jobVos, jobVo)
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, req.Page, req.PageSize, jobVos))
|
||||
}
|
||||
|
||||
// Remove 删除任务
|
||||
func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
jobId := h.GetInt(c, "id", 0)
|
||||
if jobId == 0 {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取任务,判断状态
|
||||
job, err := h.jimengService.GetJob(uint(jobId))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if job.UserId != user.Id {
|
||||
resp.ERROR(c, "无权限操作")
|
||||
return
|
||||
}
|
||||
|
||||
// 正在运行中的任务不能删除
|
||||
if job.Status == types.JMTaskStatusGenerating || job.Status == types.JMTaskStatusInQueue {
|
||||
resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||
logger.Errorf("delete jimeng job failed: %v", err)
|
||||
resp.ERROR(c, "删除任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{})
|
||||
}
|
||||
|
||||
// Retry 重试任务
|
||||
func (h *JimengHandler) Retry(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
jobId := h.GetInt(c, "id", 0)
|
||||
if jobId == 0 {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查任务是否存在且属于当前用户
|
||||
job, err := h.jimengService.GetJob(uint(jobId))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if job.UserId != userId {
|
||||
resp.ERROR(c, "无权限操作")
|
||||
return
|
||||
}
|
||||
|
||||
// 只有失败的任务才能重试
|
||||
if job.Status != types.JMTaskStatusFailed {
|
||||
resp.ERROR(c, "只有失败的任务才能重试")
|
||||
return
|
||||
}
|
||||
|
||||
// 重置任务状态
|
||||
if err := h.jimengService.UpdateJobStatus(uint(jobId), types.JMTaskStatusInQueue, ""); err != nil {
|
||||
logger.Errorf("reset job status failed: %v", err)
|
||||
resp.ERROR(c, "重置任务状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 重新推送到队列
|
||||
if err := h.jimengService.PushTaskToQueue(uint(jobId)); err != nil {
|
||||
logger.Errorf("push retry task to queue failed: %v", err)
|
||||
resp.ERROR(c, "推送重试任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
|
||||
}
|
||||
|
||||
func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
|
||||
logger.Debugf("getTaskPower req: %+v", req)
|
||||
config := h.App.SysConfig.Jimeng
|
||||
basePower, ok := config.Powers[req.ReqKey]
|
||||
if !ok || basePower <= 0 {
|
||||
return 0, errors.New("未配置模型积分或配置不合法")
|
||||
}
|
||||
switch req.TaskType {
|
||||
case types.JMTaskTypeImage:
|
||||
return basePower, nil
|
||||
case types.JMTaskTypeVideo:
|
||||
if req.Duration == 0 {
|
||||
return 0, errors.New("视频时长不能为0")
|
||||
}
|
||||
return basePower * req.Duration, nil
|
||||
case types.JMTaskTypeVirtualHuman:
|
||||
if req.AudioURL == "" {
|
||||
return 0, errors.New("音频URL不能为空")
|
||||
}
|
||||
audioDuration, err := utils.AudioDurationFromURL(req.AudioURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
seconds := int(audioDuration.Seconds())
|
||||
if seconds <= 0 {
|
||||
return 0, errors.New("音频时长无效")
|
||||
}
|
||||
return basePower * seconds, nil
|
||||
case types.JMTaskTypeActionTransfer:
|
||||
if req.VideoURL == "" {
|
||||
return 0, errors.New("视频URL不能为空")
|
||||
}
|
||||
videoDuration, err := utils.VideoDurationMP4FromURL(req.VideoURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
seconds := int(videoDuration.Seconds())
|
||||
if seconds <= 0 {
|
||||
return 0, errors.New("视频时长无效")
|
||||
}
|
||||
return basePower * seconds, nil
|
||||
default:
|
||||
return 0, errors.New("任务类型不支持")
|
||||
}
|
||||
}
|
||||
|
||||
// GetPowerConfig 获取即梦各任务类型算力消耗配置
|
||||
func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
|
||||
config := h.App.SysConfig.Jimeng
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"powers": config.Powers,
|
||||
})
|
||||
}
|
||||
@@ -10,6 +10,7 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
@@ -35,6 +36,17 @@ func NewMarkMapHandler(app *core.AppServer, db *gorm.DB, userService *service.Us
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MarkMapHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/markMap/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("gen", h.Generate)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成思维导图
|
||||
func (h *MarkMapHandler) Generate(c *gin.Context) {
|
||||
var data struct {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -25,6 +26,13 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
return &MenuHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MenuHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/menu/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("list/all", h.ListAll)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *MenuHandler) List(c *gin.Context) {
|
||||
index := h.GetBool(c, "index")
|
||||
@@ -33,7 +41,7 @@ func (h *MenuHandler) List(c *gin.Context) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
session = session.Where("enabled", true)
|
||||
if index {
|
||||
session = session.Where("id IN ?", h.App.SysConfig.IndexNavs)
|
||||
session = session.Where("id IN ?", h.App.SysConfig.Base.IndexNavs)
|
||||
}
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
@@ -47,3 +55,20 @@ func (h *MenuHandler) List(c *gin.Context) {
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
// ListAll 获取所有的菜单列表
|
||||
func (h *MenuHandler) ListAll(c *gin.Context) {
|
||||
var items []model.Menu
|
||||
var list = make([]vo.Menu, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Menu
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
list = append(list, product)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
+176
-20
@@ -10,9 +10,11 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
@@ -27,18 +29,20 @@ import (
|
||||
|
||||
type MidJourneyHandler struct {
|
||||
BaseHandler
|
||||
mjService *mj.Service
|
||||
snowflake *service.Snowflake
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
mjService *mj.Service
|
||||
snowflake *service.Snowflake
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService) *MidJourneyHandler {
|
||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *MidJourneyHandler {
|
||||
return &MidJourneyHandler{
|
||||
snowflake: snowflake,
|
||||
mjService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
snowflake: snowflake,
|
||||
mjService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
@@ -46,6 +50,26 @@ func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.S
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *MidJourneyHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/mj/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("image", h.Image)
|
||||
group.POST("upscale", h.Upscale)
|
||||
group.POST("variation", h.Variation)
|
||||
group.POST("modal", h.Modal)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
@@ -53,13 +77,37 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.MjPower {
|
||||
if user.Power < h.App.SysConfig.Base.MjPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// preCheckPower 检查用户算力是否 >= required,不足时写 ERROR 并返回 false
|
||||
func (h *MidJourneyHandler) preCheckPower(c *gin.Context, required int) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return false
|
||||
}
|
||||
if required <= 0 {
|
||||
required = h.App.SysConfig.Base.MjActionPower
|
||||
}
|
||||
if user.Power < required {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次操作!")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// mjActionPower 取分项算力,若未配置则回退 MjActionPower
|
||||
func mjActionPower(base int, fallback int) int {
|
||||
if base > 0 {
|
||||
return base
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
@@ -86,10 +134,40 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if !h.preCheck(c) {
|
||||
// 按任务类型计算所需算力
|
||||
power := h.App.SysConfig.Base.MjPower
|
||||
if data.TaskType == types.TaskBlend.String() {
|
||||
power = mjActionPower(h.App.SysConfig.Base.MjBlendPower, h.App.SysConfig.Base.MjActionPower)
|
||||
} else if data.TaskType == types.TaskSwapFace.String() {
|
||||
power = mjActionPower(h.App.SysConfig.Base.MjSwapFacePower, h.App.SysConfig.Base.MjActionPower)
|
||||
}
|
||||
if !h.preCheckPower(c, power) {
|
||||
return
|
||||
}
|
||||
|
||||
// 文本审核
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceMJ,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var params = ""
|
||||
if data.Rate != "" && !strings.Contains(params, "--ar") {
|
||||
params += " --ar " + data.Rate
|
||||
@@ -159,8 +237,8 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
ImgArr: data.ImgArr,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: data.TaskType,
|
||||
@@ -169,7 +247,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
Prompt: fmt.Sprintf("%s %s", data.Prompt, params),
|
||||
Power: h.App.SysConfig.MjPower,
|
||||
Power: power,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
opt := "绘图"
|
||||
@@ -218,7 +296,8 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !h.preCheck(c) {
|
||||
power := mjActionPower(h.App.SysConfig.Base.MjUpscalePower, h.App.SysConfig.Base.MjActionPower)
|
||||
if !h.preCheckPower(c, power) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -232,7 +311,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
Index: data.Index,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskUpscale.String(),
|
||||
@@ -240,7 +319,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.MjActionPower,
|
||||
Power: power,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
|
||||
@@ -273,7 +352,8 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !h.preCheck(c) {
|
||||
power := mjActionPower(h.App.SysConfig.Base.MjUpscalePower, h.App.SysConfig.Base.MjActionPower)
|
||||
if !h.preCheckPower(c, power) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -287,7 +367,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
ChannelId: data.ChannelId,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskVariation.String(),
|
||||
@@ -296,7 +376,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.MjActionPower,
|
||||
Power: power,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
|
||||
@@ -320,6 +400,81 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// modalReq 局部重绘请求参数
|
||||
type modalReq struct {
|
||||
TaskId string `json:"task_id"` // 原图任务 ID(message_id)
|
||||
ChannelId string `json:"channel_id"` // 渠道 ID
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
MaskBase64 string `json:"mask_base64,omitempty"` // 蒙版 base64,可选
|
||||
}
|
||||
|
||||
// Modal 提交局部重绘(inpaint)
|
||||
func (h *MidJourneyHandler) Modal(c *gin.Context) {
|
||||
var data modalReq
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if data.TaskId == "" || data.ChannelId == "" {
|
||||
resp.ERROR(c, "task_id 与 channel_id 必填")
|
||||
return
|
||||
}
|
||||
if data.Prompt == "" {
|
||||
resp.ERROR(c, "请填写局部重绘提示词")
|
||||
return
|
||||
}
|
||||
|
||||
power := mjActionPower(h.App.SysConfig.Base.MjModalPower, h.App.SysConfig.Base.MjActionPower)
|
||||
if !h.preCheckPower(c, power) {
|
||||
return
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
taskId, _ := h.snowflake.Next(true)
|
||||
// 原图 message_id 必须传入 API,同时写入 TaskId/MessageId 避免序列化 omitempty 丢失
|
||||
task := types.MjTask{
|
||||
Type: types.TaskModal,
|
||||
UserId: userId,
|
||||
ChannelId: data.ChannelId,
|
||||
TaskId: data.TaskId,
|
||||
MessageId: data.TaskId,
|
||||
Prompt: data.Prompt,
|
||||
MaskBase64: data.MaskBase64,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskModal.String(),
|
||||
ChannelId: data.ChannelId,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task.Id = job.Id
|
||||
h.mjService.PushTask(task)
|
||||
|
||||
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("局部重绘操作,任务ID:%s", job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ImgWall 照片墙
|
||||
func (h *MidJourneyHandler) ImgWall(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 0)
|
||||
@@ -386,6 +541,7 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
return nil, vo.NewPage(total, page, pageSize, jobs)
|
||||
|
||||
@@ -9,6 +9,7 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
@@ -32,6 +33,22 @@ func NewNetHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManage
|
||||
return &NetHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *NetHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/upload")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("", h.Upload)
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
}
|
||||
|
||||
// 公开接口,不需要授权
|
||||
h.App.Engine.GET("/api/download", h.Download)
|
||||
}
|
||||
|
||||
func (h *NetHandler) Upload(c *gin.Context) {
|
||||
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
||||
if err != nil {
|
||||
@@ -144,7 +161,15 @@ func (h *NetHandler) Download(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// 使用http.Get下载文件
|
||||
r, err := http.Get(fileUrl)
|
||||
req, err := http.NewRequest("GET", fileUrl, nil)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 模拟浏览器 UA
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
client := &http.Client{}
|
||||
r, err := client.Do(req)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -157,6 +182,5 @@ func (h *NetHandler) Download(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
// 将下载的文件内容写入响应
|
||||
_, _ = io.Copy(c.Writer, r.Body)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -28,6 +28,18 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
return &OrderHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *OrderHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/order/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
group.GET("query", h.Query)
|
||||
}
|
||||
}
|
||||
|
||||
// List 订单列表
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
@@ -48,20 +60,21 @@ func (h *OrderHandler) List(c *gin.Context) {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
payMethod, ok := types.PayMethods[item.PayWay]
|
||||
payChannel, ok := types.PayChannel[item.Channel]
|
||||
if !ok {
|
||||
payMethod = item.PayWay
|
||||
payChannel = item.PayWay
|
||||
}
|
||||
payName, ok := types.PayNames[item.PayType]
|
||||
payWays, ok := types.PayWays[item.PayWay]
|
||||
if !ok {
|
||||
payName = item.PayWay
|
||||
payWays = item.PayWay
|
||||
}
|
||||
order.PayMethod = payMethod
|
||||
order.PayName = payName
|
||||
order.ChannelName = payChannel
|
||||
order.PayName = payWays
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
|
||||
@@ -82,17 +95,8 @@ func (h *OrderHandler) Query(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
counter := 0
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
var item model.Order
|
||||
h.DB.Where("order_no = ?", orderNo).First(&item)
|
||||
if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status {
|
||||
order.Status = item.Status
|
||||
break
|
||||
}
|
||||
counter++
|
||||
}
|
||||
var item model.Order
|
||||
h.DB.Where("order_no = ?", orderNo).First(&item)
|
||||
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
}
|
||||
|
||||
+279
-241
@@ -11,6 +11,7 @@ import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
@@ -33,52 +34,149 @@ type PayWay struct {
|
||||
// PaymentHandler 支付服务回调 handler
|
||||
type PaymentHandler struct {
|
||||
BaseHandler
|
||||
alipayService *payment.AlipayService
|
||||
huPiPayService *payment.HuPiPayService
|
||||
geekPayService *payment.GeekPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
snowflake *service.Snowflake
|
||||
userService *service.UserService
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
signKey string // 用来签名的随机秘钥
|
||||
alipayService *payment.AlipayService
|
||||
epayService *payment.EPayService
|
||||
wxpayService *payment.WxPayService
|
||||
snowflake *service.Snowflake
|
||||
userService *service.UserService
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
config *types.PaymentConfig
|
||||
}
|
||||
|
||||
func NewPaymentHandler(
|
||||
server *core.AppServer,
|
||||
alipayService *payment.AlipayService,
|
||||
huPiPayService *payment.HuPiPayService,
|
||||
geekPayService *payment.GeekPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
geekPayService *payment.EPayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
db *gorm.DB,
|
||||
userService *service.UserService,
|
||||
snowflake *service.Snowflake,
|
||||
fs embed.FS) *PaymentHandler {
|
||||
fs embed.FS,
|
||||
sysConfig *types.SystemConfig) *PaymentHandler {
|
||||
return &PaymentHandler{
|
||||
alipayService: alipayService,
|
||||
huPiPayService: huPiPayService,
|
||||
geekPayService: geekPayService,
|
||||
wechatPayService: wechatPayService,
|
||||
snowflake: snowflake,
|
||||
userService: userService,
|
||||
fs: fs,
|
||||
lock: sync.Mutex{},
|
||||
alipayService: alipayService,
|
||||
epayService: geekPayService,
|
||||
wxpayService: wxpayService,
|
||||
snowflake: snowflake,
|
||||
userService: userService,
|
||||
fs: fs,
|
||||
lock: sync.Mutex{},
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
DB: db,
|
||||
},
|
||||
signKey: utils.RandString(32),
|
||||
config: &sysConfig.Payment,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *PaymentHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/payment/")
|
||||
|
||||
// 支付回调接口(公开)
|
||||
rg.POST("notify/alipay", h.AlipayNotify)
|
||||
rg.GET("notify/epay", h.EPayNotify)
|
||||
rg.POST("notify/wxpay", h.WxpayNotify)
|
||||
|
||||
// 需要用户登录的接口
|
||||
rg.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
rg.POST("create", h.CreateOrder)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) StartSyncOrders() {
|
||||
go func() {
|
||||
for {
|
||||
err := h.SyncOrders()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SyncOrders 同步订单状态
|
||||
func (h *PaymentHandler) SyncOrders() error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Errorf("同步订单状态发生异常: %v", err)
|
||||
}
|
||||
}()
|
||||
var orders []model.Order
|
||||
err := h.DB.Where("status", types.OrderNotPaid).Where("checked", false).Find(&orders).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, order := range orders {
|
||||
time.Sleep(time.Second * 1)
|
||||
//超时15分钟的订单,直接标记为已关闭
|
||||
if time.Now().After(order.CreatedAt.Add(time.Minute * 5)) {
|
||||
h.DB.Model(&model.Order{}).Where("id", order.Id).Update("checked", true)
|
||||
logger.Errorf("订单超时:%v", order)
|
||||
continue
|
||||
}
|
||||
// 查询订单状态
|
||||
var res payment.OrderInfo
|
||||
switch order.Channel {
|
||||
case payment.PayChannelEpay:
|
||||
res, err = h.epayService.Query(order.OrderNo)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
}
|
||||
// 微信支付
|
||||
case payment.PayChannelWX:
|
||||
res, err = h.wxpayService.Query(order.OrderNo)
|
||||
logger.Debugf("微信支付订单状态:%+v", res)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
}
|
||||
case payment.PayChannelAL:
|
||||
res, err = h.alipayService.Query(order.OrderNo)
|
||||
logger.Debugf("支付宝支付订单状态:%+v", res)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query order info: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 订单已关闭
|
||||
if res.Closed() {
|
||||
h.DB.Model(&model.Order{}).Where("id", order.Id).Updates(map[string]any{
|
||||
"checked": true,
|
||||
"status": types.OrderPaidFailed,
|
||||
})
|
||||
logger.Errorf("订单已关闭:%v", order)
|
||||
continue
|
||||
}
|
||||
|
||||
// 订单未支付,不处理,继续轮询
|
||||
if !res.Success() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 订单支付成功
|
||||
err = h.paySuccess(res)
|
||||
if err != nil {
|
||||
logger.Errorf("error with deal order: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) CreateOrder(c *gin.Context) {
|
||||
var data struct {
|
||||
PayWay string `json:"pay_way"`
|
||||
PayType string `json:"pay_type"`
|
||||
ProductId int `json:"product_id"`
|
||||
UserId int `json:"user_id"`
|
||||
Device string `json:"device"`
|
||||
Host string `json:"host"`
|
||||
PayWay string `json:"pay_way,omitempty"` // 支付方式:支付宝,微信
|
||||
Pid int `json:"pid,omitempty"`
|
||||
Device string `json:"device,omitempty"`
|
||||
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -86,7 +184,7 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
}
|
||||
|
||||
var product model.Product
|
||||
err := h.DB.Where("id", data.ProductId).First(&product).Error
|
||||
err := h.DB.Where("id", data.Pid).First(&product).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
@@ -97,136 +195,122 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
userId := h.GetLoginUserId(c)
|
||||
var user model.User
|
||||
err = h.DB.Where("id", data.UserId).First(&user).Error
|
||||
err = h.DB.Where("id", userId).First(&user).Error
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
amount := product.Discount
|
||||
var payURL, returnURL, notifyURL string
|
||||
amount := product.Price
|
||||
var payURL, notifyURL string
|
||||
switch data.PayWay {
|
||||
case "alipay":
|
||||
if h.App.Config.AlipayConfig.NotifyURL != "" { // 用于本地调试支付
|
||||
notifyURL = h.App.Config.AlipayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Host)
|
||||
}
|
||||
if h.App.Config.AlipayConfig.ReturnURL != "" { // 用于本地调试支付
|
||||
returnURL = h.App.Config.AlipayConfig.ReturnURL
|
||||
} else {
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
money := fmt.Sprintf("%.2f", amount)
|
||||
if data.Device == "wechat" {
|
||||
payURL, err = h.alipayService.PayMobile(payment.AlipayParams{
|
||||
case "wxpay":
|
||||
logger.Debugf("微信支付,%+v", data)
|
||||
data.Channel = payment.PayChannelWX
|
||||
// 优先使用微信官方支付
|
||||
if h.config.WxPay.Enabled {
|
||||
data.Channel = "wxpay"
|
||||
if h.config.WxPay.Domain != "" {
|
||||
data.Domain = h.config.WxPay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/wxpay", data.Domain)
|
||||
params := payment.PayRequest{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: money,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
} else {
|
||||
payURL, err = h.alipayService.PayPC(payment.AlipayParams{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: money,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
return
|
||||
}
|
||||
break
|
||||
case "wechat":
|
||||
if h.App.Config.WechatPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.WechatPayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/wechat", data.Host)
|
||||
}
|
||||
if data.Device == "wechat" {
|
||||
payURL, err = h.wechatPayService.PayUrlH5(payment.WechatPayParams{
|
||||
OutTradeNo: orderNo,
|
||||
TotalFee: int(amount * 100),
|
||||
TotalFee: fmt.Sprintf("%d", int(amount*100)),
|
||||
Subject: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
ClientIP: c.ClientIP(),
|
||||
})
|
||||
} else {
|
||||
payURL, err = h.wechatPayService.PayUrlNative(payment.WechatPayParams{
|
||||
Device: data.Device,
|
||||
PayWay: payment.PayWayWX,
|
||||
}
|
||||
if data.Device == "mobile" {
|
||||
params.OpenID = user.OpenId
|
||||
}
|
||||
payURL, err = h.wxpayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
} else if h.config.Epay.Enabled { // 聚合支付
|
||||
logger.Debugf("聚合支付%+v", data)
|
||||
data.Channel = payment.PayChannelEpay
|
||||
if h.config.Epay.Domain != "" {
|
||||
data.Domain = h.config.Epay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/epay", data.Domain)
|
||||
params := payment.PayRequest{
|
||||
OutTradeNo: orderNo,
|
||||
TotalFee: int(amount * 100),
|
||||
Subject: product.Name,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
PayWay: payment.PayWayWX,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
|
||||
r, err := h.epayService.Pay(params)
|
||||
logger.Debugf("请求支付结果,%+v", r)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
} else {
|
||||
payURL = r
|
||||
}
|
||||
} else {
|
||||
resp.ERROR(c, "系统没有配置可用的支付渠道!")
|
||||
return
|
||||
}
|
||||
case "alipay":
|
||||
if h.config.Alipay.Enabled {
|
||||
logger.Debugf("支付宝,%+v", data)
|
||||
data.Channel = payment.PayChannelAL
|
||||
if h.config.Alipay.Domain != "" { // 用于本地调试支付
|
||||
data.Domain = h.config.Alipay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Domain)
|
||||
money := fmt.Sprintf("%.2f", amount)
|
||||
payURL, err = h.alipayService.Pay(payment.PayRequest{
|
||||
Device: data.Device,
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: money,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
break
|
||||
case "hupi":
|
||||
if h.App.Config.HuPiPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/hupi", data.Host)
|
||||
}
|
||||
if h.App.Config.HuPiPayConfig.ReturnURL != "" {
|
||||
returnURL = h.App.Config.HuPiPayConfig.ReturnURL
|
||||
} else {
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
r, err := h.huPiPayService.Pay(payment.HuPiPayParams{
|
||||
Version: "1.1",
|
||||
TradeOrderId: orderNo,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
Title: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
ReturnURL: returnURL,
|
||||
WapName: "GeekAI助手",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
payURL = r.URL
|
||||
break
|
||||
case "geek":
|
||||
if h.App.Config.GeekPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.GeekPayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/geek", data.Host)
|
||||
}
|
||||
if h.App.Config.GeekPayConfig.ReturnURL != "" {
|
||||
data.Host = utils.GetBaseURL(h.App.Config.GeekPayConfig.ReturnURL)
|
||||
}
|
||||
if data.Device == "wechat" { // 微信客户端打开,调回手机端用户中心页面
|
||||
returnURL = fmt.Sprintf("%s/mobile/profile", data.Host)
|
||||
} else {
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
params := payment.GeekPayParams{
|
||||
OutTradeNo: orderNo,
|
||||
Method: "web",
|
||||
Name: product.Name,
|
||||
Money: fmt.Sprintf("%f", amount),
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
Type: data.PayType,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
|
||||
res, err := h.geekPayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else if h.config.Epay.Enabled { // 聚合支付
|
||||
logger.Debugf("聚合支付,%+v", data)
|
||||
data.Channel = payment.PayChannelEpay
|
||||
if h.config.Epay.Domain != "" {
|
||||
data.Domain = h.config.Epay.Domain
|
||||
}
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/epay", data.Domain)
|
||||
params := payment.PayRequest{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
PayWay: data.PayWay,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
|
||||
r, err := h.epayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
} else {
|
||||
payURL = r
|
||||
}
|
||||
} else {
|
||||
resp.ERROR(c, "系统没有配置可用的支付渠道!")
|
||||
return
|
||||
}
|
||||
payURL = res.PayURL
|
||||
default:
|
||||
resp.ERROR(c, "不支持的支付渠道")
|
||||
return
|
||||
@@ -234,43 +318,40 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Days: product.Days,
|
||||
Power: product.Power,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
Discount: product.Discount,
|
||||
Power: product.Power,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
}
|
||||
order := model.Order{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
ProductId: product.Id,
|
||||
OrderNo: orderNo,
|
||||
Subject: product.Name,
|
||||
Amount: amount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: data.PayWay,
|
||||
PayType: data.PayType,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
OrderNo: orderNo,
|
||||
Subject: product.Name,
|
||||
Amount: amount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: data.PayWay,
|
||||
Channel: data.Channel,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
}
|
||||
err = h.DB.Create(&order).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with create order: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, payURL)
|
||||
resp.SUCCESS(c, gin.H{"pay_url": payURL, "order_no": orderNo})
|
||||
}
|
||||
|
||||
// 异步通知回调公共逻辑
|
||||
func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
// 支付成功处理
|
||||
func (h *PaymentHandler) paySuccess(info payment.OrderInfo) error {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
var order model.Order
|
||||
err := h.DB.Where("order_no = ?", orderNo).First(&order).Error
|
||||
err := h.DB.Where("order_no", info.OutTradeNo).First(&order).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with fetch order: %v", err)
|
||||
}
|
||||
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
// 已支付订单,直接返回
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
return nil
|
||||
@@ -290,19 +371,21 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
|
||||
// 增加用户算力
|
||||
err = h.userService.IncreasePower(order.UserId, remark.Power, model.PowerLog{
|
||||
Type: types.PowerRecharge,
|
||||
Model: order.PayWay,
|
||||
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
|
||||
Type: types.PowerRecharge,
|
||||
Model: order.Subject,
|
||||
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
order.PayTime = time.Now().Unix()
|
||||
order.PayTime = utils.Str2stamp(info.PayTime)
|
||||
order.Status = types.OrderPaidSuccess
|
||||
order.TradeNo = tradeNo
|
||||
err = h.DB.Updates(&order).Error
|
||||
order.TradeNo = info.TradeId
|
||||
order.Checked = true
|
||||
err = h.DB.Debug().Updates(&order).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with update order info: %v", err)
|
||||
}
|
||||
@@ -317,54 +400,6 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPayWays 获取支付方式
|
||||
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
|
||||
payWays := make([]gin.H, 0)
|
||||
if h.App.Config.AlipayConfig.Enabled {
|
||||
payWays = append(payWays, gin.H{"pay_way": "alipay", "pay_type": "alipay"})
|
||||
}
|
||||
if h.App.Config.HuPiPayConfig.Enabled {
|
||||
payWays = append(payWays, gin.H{"pay_way": "hupi", "pay_type": "wxpay"})
|
||||
}
|
||||
if h.App.Config.GeekPayConfig.Enabled {
|
||||
for _, v := range h.App.Config.GeekPayConfig.Methods {
|
||||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": v})
|
||||
}
|
||||
}
|
||||
if h.App.Config.WechatPayConfig.Enabled {
|
||||
payWays = append(payWays, gin.H{"pay_way": "wechat", "pay_type": "wxpay"})
|
||||
}
|
||||
resp.SUCCESS(c, payWays)
|
||||
}
|
||||
|
||||
// HuPiPayNotify 虎皮椒支付异步回调
|
||||
func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := c.Request.Form.Get("trade_order_id")
|
||||
tradeNo := c.Request.Form.Get("open_order_id")
|
||||
logger.Infof("收到虎皮椒订单支付回调,%+v", c.Request.Form)
|
||||
|
||||
if err = h.huPiPayService.Check(orderNo); err != nil {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.notify(orderNo, tradeNo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// AlipayNotify 支付宝支付回调
|
||||
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
@@ -373,16 +408,15 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result := h.alipayService.TradeVerify(c.Request)
|
||||
logger.Infof("收到支付宝商号订单支付回调:%+v", result)
|
||||
if !result.Success() {
|
||||
logger.Error("订单校验失败:", result.Message)
|
||||
orderInfo, err := h.alipayService.Query(c.Request.Form.Get("out_trade_no"))
|
||||
logger.Infof("收到支付宝商号订单支付回调:%+v", orderInfo)
|
||||
if !orderInfo.Success() {
|
||||
logger.Errorf("订单校验失败:%v", err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
tradeNo := c.Request.Form.Get("trade_no")
|
||||
err = h.notify(result.OutTradeNo, tradeNo)
|
||||
err = h.paySuccess(orderInfo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
@@ -392,28 +426,35 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// GeekPayNotify 支付异步回调
|
||||
func (h *PaymentHandler) GeekPayNotify(c *gin.Context) {
|
||||
// EPayNotify 易支付支付异步回调
|
||||
func (h *PaymentHandler) EPayNotify(c *gin.Context) {
|
||||
var params = make(map[string]string)
|
||||
for k := range c.Request.URL.Query() {
|
||||
params[k] = c.Query(k)
|
||||
}
|
||||
|
||||
logger.Infof("收到GeekPay订单支付回调:%+v", params)
|
||||
// 检查支付状态
|
||||
logger.Infof("收到易支付订单支付回调:%+v", params)
|
||||
// 检查支付状态, 如果未支付,则返回成功
|
||||
if params["trade_status"] != "TRADE_SUCCESS" {
|
||||
c.String(http.StatusOK, "success")
|
||||
return
|
||||
}
|
||||
|
||||
sign := h.geekPayService.Sign(params)
|
||||
sign := h.epayService.Sign(params)
|
||||
if sign != c.Query("sign") {
|
||||
logger.Errorf("签名验证失败, %s, %s", sign, c.Query("sign"))
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
// 查询订单状态
|
||||
order, err := h.epayService.Query(params["out_trade_no"])
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.notify(params["out_trade_no"], params["trade_no"])
|
||||
err = h.paySuccess(order)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
@@ -423,26 +464,23 @@ func (h *PaymentHandler) GeekPayNotify(c *gin.Context) {
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// WechatPayNotify 微信商户支付异步回调
|
||||
func (h *PaymentHandler) WechatPayNotify(c *gin.Context) {
|
||||
// WxpayNotify 微信商户支付异步回调
|
||||
func (h *PaymentHandler) WxpayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
result := h.wechatPayService.TradeVerify(c.Request)
|
||||
logger.Infof("收到微信商号订单支付回调:%+v", result)
|
||||
if !result.Success() {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": "FAIL",
|
||||
"message": err.Error(),
|
||||
})
|
||||
orderInfo, err := h.wxpayService.TradeVerify(c.Request)
|
||||
logger.Infof("收到微信商号订单支付回调:%+v", orderInfo)
|
||||
if err != nil {
|
||||
logger.Errorf("订单校验失败:%v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.notify(result.OutTradeNo, result.TradeId)
|
||||
err = h.paySuccess(orderInfo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
|
||||
@@ -9,11 +9,13 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -27,6 +29,18 @@ func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
|
||||
return &PowerLogHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *PowerLogHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/powerLog/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("list", h.List)
|
||||
group.GET("stats", h.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PowerLogHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Model string `json:"model"`
|
||||
@@ -72,3 +86,45 @@ func (h *PowerLogHandler) List(c *gin.Context) {
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
// Stats 获取用户算力统计
|
||||
func (h *PowerLogHandler) Stats(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息(包含余额)
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
resp.ERROR(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 计算总消费(所有支出记录)
|
||||
var totalConsume int64
|
||||
h.DB.Model(&model.PowerLog{}).
|
||||
Where("user_id", userId).
|
||||
Where("mark", types.PowerSub).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&totalConsume)
|
||||
|
||||
// 计算今日消费
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var todayConsume int64
|
||||
h.DB.Model(&model.PowerLog{}).
|
||||
Where("user_id", userId).
|
||||
Where("mark", types.PowerSub).
|
||||
Where("DATE(created_at) = ?", today).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&todayConsume)
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total": totalConsume,
|
||||
"today": todayConsume,
|
||||
"balance": user.Power,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, stats)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/ppt"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PPTTaskHandler 用户侧 PPT 生成任务处理器(薄层:参数、鉴权、调用 PptService)
|
||||
type PPTTaskHandler struct {
|
||||
BaseHandler
|
||||
snowflake *service.Snowflake
|
||||
pptService *ppt.PptService
|
||||
}
|
||||
|
||||
// NewPPTTaskHandler 创建 PPT 任务处理器
|
||||
func NewPPTTaskHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, pptService *ppt.PptService) *PPTTaskHandler {
|
||||
return &PPTTaskHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
snowflake: snowflake,
|
||||
pptService: pptService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册 PPT 任务相关路由
|
||||
func (h *PPTTaskHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/v1/tasks")
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("generate-slides", h.CreateTask)
|
||||
group.POST("generate-slides/from-file", h.CreateTaskFromFile)
|
||||
group.GET("", h.ListTasks) // 当前用户任务列表,须在 :task_id 前注册
|
||||
group.GET(":task_id/export", h.ExportTask)
|
||||
group.POST(":task_id/resume", h.ResumeTask)
|
||||
group.POST(":task_id/slides/:slide_index/edit-image", h.EditSlideImage)
|
||||
group.PATCH(":task_id/slides/:slide_index/active-image", h.SetActiveSlideImage)
|
||||
group.GET(":task_id", h.GetTask)
|
||||
group.DELETE(":task_id", h.DeleteTask)
|
||||
}
|
||||
}
|
||||
|
||||
type createPPTTaskRequest struct {
|
||||
Content string `json:"content"`
|
||||
Prompt string `json:"prompt"`
|
||||
Language string `json:"language"` // 如 zh-CN, en,约束分镜输出语言
|
||||
Pages int `json:"pages"` // 目标页数,0 表示用配置默认值
|
||||
Mode string `json:"mode"` // 生成模式:detailed=详细演示文稿,slides=演示用幻灯片,空则默认 slides
|
||||
}
|
||||
|
||||
type createPPTTaskFromFileRequest struct {
|
||||
FileURL string `json:"file_url"`
|
||||
Prompt string `json:"prompt"`
|
||||
Language string `json:"language"` // 如 zh-CN, en,约束分镜输出语言
|
||||
Pages int `json:"pages"` // 目标页数,0 表示用配置默认值
|
||||
Mode string `json:"mode"` // 生成模式:detailed=详细演示文稿,slides=演示用幻灯片,空则默认 slides
|
||||
}
|
||||
|
||||
// CreateTask 创建 PPT 生成任务(异步)
|
||||
func (h *PPTTaskHandler) CreateTask(c *gin.Context) {
|
||||
var req createPPTTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Content == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
taskID, err := h.snowflake.Next(true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "生成任务ID失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task, cfg, err := h.pptService.BuildPendingTask(taskID, user.Id, int(user.Power), req.Content, req.Prompt, req.Language, req.Mode, req.Pages)
|
||||
if err != nil {
|
||||
if errors.Is(err, ppt.ErrInsufficientPower) {
|
||||
resp.ERROR(c, "当前用户算力不足以完成本次 PPT 生成任务!")
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, "加载 PPT 配置或校验失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.pptService.CreateTask(c.Request.Context(), task, cfg)
|
||||
go h.pptService.RunTask(context.Background(), task, cfg)
|
||||
|
||||
resp.SUCCESS(c, map[string]any{
|
||||
"task_id": taskID,
|
||||
"status": ppt.TaskStatusPending,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateTaskFromFile 创建 PPT 生成任务(基于上传材料文本提炼)
|
||||
// 支持文件:PDF、Word(doc/docx)、TXT、Markdown(md/markdown)
|
||||
func (h *PPTTaskHandler) CreateTaskFromFile(c *gin.Context) {
|
||||
var req createPPTTaskFromFileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.FileURL) == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
fileURL := strings.TrimSpace(req.FileURL)
|
||||
|
||||
// 去掉 query,避免 ".pdf?xxx" 这种导致 ext 识别失败
|
||||
urlNoQuery := strings.Split(fileURL, "?")[0]
|
||||
ext := strings.ToLower(filepath.Ext(urlNoQuery))
|
||||
if ext == "" {
|
||||
resp.ERROR(c, "不支持的文件格式")
|
||||
return
|
||||
}
|
||||
|
||||
allowedExts := map[string]bool{
|
||||
".pdf": true,
|
||||
".doc": true,
|
||||
".docx": true,
|
||||
".txt": true,
|
||||
".md": true,
|
||||
".markdown": true,
|
||||
}
|
||||
if !allowedExts[ext] {
|
||||
resp.ERROR(c, "不支持的文件格式")
|
||||
return
|
||||
}
|
||||
|
||||
designPrompt := strings.TrimSpace(req.Prompt)
|
||||
language := strings.TrimSpace(req.Language)
|
||||
if language == "" {
|
||||
language = "zh-CN"
|
||||
}
|
||||
mode := strings.TrimSpace(req.Mode)
|
||||
|
||||
pages := req.Pages
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
taskID, err := h.snowflake.Next(true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "生成任务ID失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 1) 下载文件并提取原始文本(Notebook 风格提炼前的输入)
|
||||
rawText, err := h.extractMaterialTextFromURL(c.Request.Context(), fileURL, ext)
|
||||
if err != nil || strings.TrimSpace(rawText) == "" {
|
||||
if err == nil {
|
||||
err = errors.New("empty extracted text")
|
||||
}
|
||||
resp.ERROR(c, "读取文件失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 2) 校验算力与页数,组装待写入的 Task(先在提炼前做 power 检查)
|
||||
task, cfg, err := h.pptService.BuildPendingTask(taskID, user.Id, int(user.Power), rawText, designPrompt, language, mode, pages)
|
||||
if err != nil {
|
||||
if errors.Is(err, ppt.ErrInsufficientPower) {
|
||||
resp.ERROR(c, "当前用户算力不足以完成本次 PPT 生成任务!")
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, "加载 PPT 配置或校验失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 3) NotebookLM 风格提炼:rawText -> PPT 可用的 content(大纲/结构化要点)
|
||||
notebookContent, err := h.pptService.GenerateNotebookContent(c.Request.Context(), cfg, rawText, designPrompt, language)
|
||||
if err != nil || strings.TrimSpace(notebookContent) == "" {
|
||||
if err == nil {
|
||||
err = errors.New("empty notebook content")
|
||||
}
|
||||
resp.ERROR(c, "提炼材料失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
task.Content = notebookContent
|
||||
|
||||
// 4) 落库 + 异步生成分镜与图片
|
||||
err = h.pptService.CreateTask(c.Request.Context(), task, cfg)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "创建任务失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
go h.pptService.RunTask(context.Background(), task, cfg)
|
||||
|
||||
resp.SUCCESS(c, map[string]any{
|
||||
"task_id": taskID,
|
||||
"status": ppt.TaskStatusPending,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PPTTaskHandler) extractMaterialTextFromURL(ctx context.Context, fileURL string, ext string) (string, error) {
|
||||
if strings.TrimSpace(fileURL) == "" {
|
||||
return "", errors.New("empty file url")
|
||||
}
|
||||
|
||||
switch ext {
|
||||
case ".txt", ".md", ".markdown":
|
||||
b, status, err := utils.FetchURLBytes(ctx, fileURL, "", 30*time.Second, 2, 8<<20)
|
||||
if err != nil {
|
||||
// status=0 时通常是请求阶段错误(例如 TLS 握手超时)
|
||||
return "", fmt.Errorf("download file failed: status=%d: %w", status, err)
|
||||
}
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
default:
|
||||
// PDF/Word 等走 Tika
|
||||
return utils.ReadFileContent(fileURL, h.App.Config.TikaHost)
|
||||
}
|
||||
}
|
||||
|
||||
// ListTasks 当前用户的 PPT 任务列表(分页)
|
||||
func (h *PPTTaskHandler) ListTasks(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
slice, total := h.pptService.ListUserTasks(c.Request.Context(), user.Id, page, pageSize)
|
||||
|
||||
jobs := make([]map[string]any, 0, len(slice))
|
||||
for _, t := range slice {
|
||||
job := t.TaskSummaryMap()
|
||||
job["prompt"] = t.Prompt
|
||||
if t.ErrorMessage != "" {
|
||||
job["error_message"] = t.ErrorMessage
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, map[string]any{
|
||||
"jobs": jobs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// ExportTask 导出任务幻灯片为 PDF 或 PPTX(按图片逐页)
|
||||
func (h *PPTTaskHandler) ExportTask(c *gin.Context) {
|
||||
taskID := strings.TrimSpace(c.Param("task_id"))
|
||||
if taskID == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
ef, ok := ppt.ParseExportFormat(c.Query("format"))
|
||||
if !ok {
|
||||
resp.ERROR(c, "format 参数无效,支持 pdf 或 pptx")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
task, exists := h.pptService.GetTask(taskID)
|
||||
if !exists {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if user.Id != task.UserID {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
if task.Status != ppt.TaskStatusCompleted {
|
||||
resp.ERROR(c, "仅已完成任务可导出")
|
||||
return
|
||||
}
|
||||
|
||||
ossCfg := types.OSSConfig{}
|
||||
if h.App.SysConfig != nil {
|
||||
ossCfg = h.App.SysConfig.OSS
|
||||
}
|
||||
data, err := ppt.BuildExportBytes(c.Request.Context(), task.Slides, ef, ossCfg, h.App.Config)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
base := ppt.SanitizeExportBaseName(task.Title, task.TaskID)
|
||||
filename := base + ppt.ExportFileExt(ef)
|
||||
c.Header("Content-Disposition", ppt.ContentDispositionAttachment(filename))
|
||||
c.Data(http.StatusOK, ppt.ExportMimeType(ef), data)
|
||||
}
|
||||
|
||||
// ResumeTask 继续生成缺图页(POST /api/v1/tasks/:task_id/resume)
|
||||
func (h *PPTTaskHandler) ResumeTask(c *gin.Context) {
|
||||
taskID := strings.TrimSpace(c.Param("task_id"))
|
||||
if taskID == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.pptService.ResumeTask(c.Request.Context(), taskID, user.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ppt.ErrPptTaskNotFound) {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrPptTaskBusy) {
|
||||
resp.ERROR(c, "任务正在处理中,请稍后再试")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrPptTaskNotResumable) {
|
||||
resp.ERROR(c, "当前任务无法继续生成(已完成或分镜数据不完整)")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrInsufficientPower) {
|
||||
resp.ERROR(c, "当前用户算力不足以完成剩余图片生成")
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, map[string]any{
|
||||
"task_id": taskID,
|
||||
"status": ppt.TaskStatusProcessing,
|
||||
})
|
||||
}
|
||||
|
||||
type editSlideImageRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
|
||||
type activeSlideImageRequest struct {
|
||||
VersionIndex int `json:"version_index"`
|
||||
}
|
||||
|
||||
// EditSlideImage 图生图编辑当前页(基于激活图)
|
||||
func (h *PPTTaskHandler) EditSlideImage(c *gin.Context) {
|
||||
taskID := strings.TrimSpace(c.Param("task_id"))
|
||||
slideIndexStr := strings.TrimSpace(c.Param("slide_index"))
|
||||
if taskID == "" || slideIndexStr == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
slideIndex, err := strconv.Atoi(slideIndexStr)
|
||||
if err != nil || slideIndex < 1 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var req editSlideImageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
ossCfg := types.OSSConfig{}
|
||||
if h.App.SysConfig != nil {
|
||||
ossCfg = h.App.SysConfig.OSS
|
||||
}
|
||||
|
||||
slides, err := h.pptService.EditSlideImage(c.Request.Context(), taskID, user.Id, slideIndex, req.Prompt, ossCfg, h.App.Config)
|
||||
if err != nil {
|
||||
if errors.Is(err, ppt.ErrPptTaskNotFound) {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrPptSlideNotFound) {
|
||||
resp.ERROR(c, "幻灯片不存在")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrPptSlideNoImage) {
|
||||
resp.ERROR(c, "该页暂无配图,无法编辑")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrInsufficientPower) {
|
||||
resp.ERROR(c, "当前用户算力不足以完成本次编辑")
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, map[string]any{"slides": slides})
|
||||
}
|
||||
|
||||
// SetActiveSlideImage 切换当前页激活的历史版本
|
||||
func (h *PPTTaskHandler) SetActiveSlideImage(c *gin.Context) {
|
||||
taskID := strings.TrimSpace(c.Param("task_id"))
|
||||
slideIndexStr := strings.TrimSpace(c.Param("slide_index"))
|
||||
if taskID == "" || slideIndexStr == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
slideIndex, err := strconv.Atoi(slideIndexStr)
|
||||
if err != nil || slideIndex < 1 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var req activeSlideImageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
slides, err := h.pptService.SetActiveSlideVersion(taskID, user.Id, slideIndex, req.VersionIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, ppt.ErrPptTaskNotFound) {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrPptSlideNotFound) {
|
||||
resp.ERROR(c, "幻灯片不存在")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrPptInvalidVersionIndex) {
|
||||
resp.ERROR(c, "无效的历史版本序号")
|
||||
return
|
||||
}
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, map[string]any{"slides": slides})
|
||||
}
|
||||
|
||||
// GetTask 查询 PPT 任务进度
|
||||
func (h *PPTTaskHandler) GetTask(c *gin.Context) {
|
||||
taskId := c.Param("task_id")
|
||||
if taskId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
task, ok := h.pptService.GetTask(taskId)
|
||||
if !ok {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil || user.Id != task.UserID {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
percentage := 0
|
||||
if task.Total > 0 {
|
||||
percentage = int(float64(task.Completed) / float64(task.Total) * 100)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, map[string]any{
|
||||
"task_id": task.TaskID,
|
||||
"status": task.Status,
|
||||
"progress": map[string]any{
|
||||
"total_slides": task.Total,
|
||||
"completed_slides": task.Completed,
|
||||
"percentage": percentage,
|
||||
},
|
||||
"slides": task.Slides,
|
||||
"error_message": task.ErrorMessage,
|
||||
"content": task.Content,
|
||||
"prompt": task.Prompt,
|
||||
"title": task.Title,
|
||||
"thumb": task.Thumb,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTask 删除用户 PPT 任务(仅允许 completed / failed),同时删除该任务生成的图片对象。
|
||||
func (h *PPTTaskHandler) DeleteTask(c *gin.Context) {
|
||||
taskID := c.Param("task_id")
|
||||
if taskID == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.pptService.DeleteTask(taskID, user.Id); err != nil {
|
||||
if errors.Is(err, ppt.ErrPptTaskNotFound) {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ppt.ErrPptTaskNotDeletable) {
|
||||
resp.ERROR(c, "仅允许删除已完成/已失败任务")
|
||||
return
|
||||
}
|
||||
// 前端会统一在文案里拼接“删除失败:”,这里避免重复。
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "删除成功"})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -25,6 +26,12 @@ func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||
return &ProductHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ProductHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/product/")
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
var items []model.Product
|
||||
|
||||
@@ -10,12 +10,14 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -39,6 +41,20 @@ func NewPromptHandler(app *core.AppServer, db *gorm.DB, userService *service.Use
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *PromptHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/prompt/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)).Use(middleware.RateLimitEvery(h.App.Redis, 30*time.Second))
|
||||
{
|
||||
group.POST("lyric", h.Lyric)
|
||||
group.POST("image", h.Image)
|
||||
group.POST("video", h.Video)
|
||||
group.POST("meta", h.MetaPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
// Lyric 生成歌词
|
||||
func (h *PromptHandler) Lyric(c *gin.Context) {
|
||||
var data struct {
|
||||
@@ -48,25 +64,12 @@ func (h *PromptHandler) Lyric(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成歌词",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
@@ -79,23 +82,12 @@ func (h *PromptHandler) Image(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成绘画提示词",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
}
|
||||
|
||||
@@ -108,25 +100,12 @@ func (h *PromptHandler) Video(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成视频脚本",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
}
|
||||
|
||||
@@ -158,9 +137,9 @@ func (h *PromptHandler) MetaPrompt(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *PromptHandler) getPromptModel() string {
|
||||
if h.App.SysConfig.TranslateModelId > 0 {
|
||||
if h.App.SysConfig.Base.AssistantModelId > 0 {
|
||||
var chatModel model.ChatModel
|
||||
h.DB.Where("id", h.App.SysConfig.TranslateModelId).First(&chatModel)
|
||||
h.DB.Where("id", h.App.SysConfig.Base.AssistantModelId).First(&chatModel)
|
||||
return chatModel.Value
|
||||
}
|
||||
return "gpt-4o"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
@@ -39,6 +40,18 @@ func NewRealtimeHandler(server *core.AppServer, db *gorm.DB, userService *servic
|
||||
return &RealtimeHandler{BaseHandler: BaseHandler{App: server, DB: db}, userService: userService}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *RealtimeHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/realtime/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.Any("", h.Connection)
|
||||
group.POST("voice", h.VoiceChat)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RealtimeHandler) Connection(c *gin.Context) {
|
||||
// 获取客户端请求中指定的子协议
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
@@ -154,7 +167,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.AdvanceVoicePower {
|
||||
if user.Power < h.App.SysConfig.Base.AdvanceVoicePower {
|
||||
resp.ERROR(c, "当前用户算力不足,无法使用该功能")
|
||||
return
|
||||
}
|
||||
@@ -166,7 +179,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
|
||||
logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, "advanced-voice")
|
||||
r, err := client.R().SetHeader("Body-Type", "application/json").
|
||||
r, err := client.R().SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(types.ApiRequest{
|
||||
Model: "advanced-voice",
|
||||
@@ -198,7 +211,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
|
||||
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.AdvanceVoicePower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.Base.AdvanceVoicePower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "advanced-voice",
|
||||
Remark: "实时语音通话",
|
||||
@@ -208,11 +221,12 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("Response: %v", response.Choices[0].Message.Content)
|
||||
replyText := utils.NormalizeAssistantContent(response.Choices[0].Message.Content)
|
||||
logger.Infof("Response: %v", replyText)
|
||||
|
||||
// 提取链接
|
||||
re := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
|
||||
links := re.FindAllStringSubmatch(response.Choices[0].Message.Content, -1)
|
||||
links := re.FindAllStringSubmatch(replyText, -1)
|
||||
var url = ""
|
||||
if len(links) > 0 {
|
||||
url = links[0][2]
|
||||
|
||||
@@ -10,14 +10,16 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RedeemHandler struct {
|
||||
@@ -30,6 +32,17 @@ func NewRedeemHandler(app *core.AppServer, db *gorm.DB, userService *service.Use
|
||||
return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}, userService: userService}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *RedeemHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/redeem/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("verify", h.Verify)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) Verify(c *gin.Context) {
|
||||
var data struct {
|
||||
Code string `json:"code"`
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SdJobHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
sdService *sd.Service
|
||||
uploader *oss.UploaderManager
|
||||
snowflake *service.Snowflake
|
||||
leveldb *store.LevelDB
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewSdJobHandler(app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
service *sd.Service,
|
||||
manager *oss.UploaderManager,
|
||||
snowflake *service.Snowflake,
|
||||
userService *service.UserService,
|
||||
levelDB *store.LevelDB) *SdJobHandler {
|
||||
return &SdJobHandler{
|
||||
sdService: service,
|
||||
uploader: manager,
|
||||
snowflake: snowflake,
|
||||
leveldb: levelDB,
|
||||
userService: userService,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SdJobHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.SdPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var data types.SdTaskParams
|
||||
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Width <= 0 {
|
||||
data.Width = 512
|
||||
}
|
||||
if data.Height <= 0 {
|
||||
data.Height = 512
|
||||
}
|
||||
if data.CfgScale <= 0 {
|
||||
data.CfgScale = 7
|
||||
}
|
||||
if data.Seed == 0 {
|
||||
data.Seed = -1
|
||||
}
|
||||
if data.Steps <= 0 {
|
||||
data.Steps = 20
|
||||
}
|
||||
if data.Sampler == "" {
|
||||
data.Sampler = "Euler a"
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
taskId, err := h.snowflake.Next(true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate task id: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task := types.SdTask{
|
||||
Type: types.TaskImage,
|
||||
Params: types.SdTaskParams{
|
||||
TaskId: taskId,
|
||||
Prompt: data.Prompt,
|
||||
NegPrompt: data.NegPrompt,
|
||||
Steps: data.Steps,
|
||||
Sampler: data.Sampler,
|
||||
FaceFix: data.FaceFix,
|
||||
CfgScale: data.CfgScale,
|
||||
Seed: data.Seed,
|
||||
Height: data.Height,
|
||||
Width: data.Width,
|
||||
HdFix: data.HdFix,
|
||||
HdRedrawRate: data.HdRedrawRate,
|
||||
HdScale: data.HdScale,
|
||||
HdScaleAlg: data.HdScaleAlg,
|
||||
HdSteps: data.HdSteps,
|
||||
},
|
||||
UserId: userId,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
}
|
||||
|
||||
job := model.SdJob{
|
||||
UserId: uint(userId),
|
||||
Type: types.TaskImage.String(),
|
||||
TaskId: taskId,
|
||||
Params: utils.JsonEncode(task.Params),
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Prompt: data.Prompt,
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.SdPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
res := h.DB.Create(&job)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "error with save job: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task.Id = int(job.Id)
|
||||
h.sdService.PushTask(task)
|
||||
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "stable-diffusion",
|
||||
Remark: fmt.Sprintf("绘图操作,任务ID:%s", job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ImgWall 照片墙
|
||||
func (h *SdJobHandler) ImgWall(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
err, jobs := h.getData(true, 0, page, pageSize, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 SD 任务列表
|
||||
func (h *SdJobHandler) JobList(c *gin.Context) {
|
||||
finish := h.GetBool(c, "finish")
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
publish := h.GetBool(c, "publish")
|
||||
|
||||
err, jobs := h.getData(finish, userId, page, pageSize, publish)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 MJ 任务列表
|
||||
func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress >= ?", 100).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if publish {
|
||||
session = session.Where("publish", publish)
|
||||
}
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.SdJob{}).Count(&total)
|
||||
|
||||
var items []model.SdJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, vo.Page{}
|
||||
}
|
||||
|
||||
var jobs = make([]vo.SdJob, 0)
|
||||
for _, item := range items {
|
||||
var job vo.SdJob
|
||||
err := utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return nil, vo.NewPage(total, page, pageSize, jobs)
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *SdJobHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.SdJob
|
||||
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
err := h.DB.Delete(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// remove image
|
||||
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Publish 发布/取消发布图片到画廊显示
|
||||
func (h *SdJobHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
|
||||
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
+24
-17
@@ -24,24 +24,31 @@ const CodeStorePrefix = "/verify/codes/"
|
||||
|
||||
type SmsHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
sms *sms.ServiceManager
|
||||
smtp *service.SmtpService
|
||||
captcha *service.CaptchaService
|
||||
redis *redis.Client
|
||||
sms *sms.SmsManager
|
||||
smtp *service.SmtpService
|
||||
captchaService *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewSmsHandler(
|
||||
app *core.AppServer,
|
||||
client *redis.Client,
|
||||
sms *sms.ServiceManager,
|
||||
sms *sms.SmsManager,
|
||||
smtp *service.SmtpService,
|
||||
captcha *service.CaptchaService) *SmsHandler {
|
||||
return &SmsHandler{
|
||||
redis: client,
|
||||
sms: sms,
|
||||
captcha: captcha,
|
||||
smtp: smtp,
|
||||
BaseHandler: BaseHandler{App: app}}
|
||||
redis: client,
|
||||
sms: sms,
|
||||
captchaService: captcha,
|
||||
smtp: smtp,
|
||||
BaseHandler: BaseHandler{App: app}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *SmsHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/sms/")
|
||||
// 无需授权的接口
|
||||
group.POST("code", h.SendCode)
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
@@ -56,12 +63,12 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if h.App.SysConfig.EnabledVerify {
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
@@ -72,14 +79,14 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
code := utils.RandomNumber(6)
|
||||
var err error
|
||||
if strings.Contains(data.Receiver, "@") { // email
|
||||
if !utils.Contains(h.App.SysConfig.RegisterWays, "email") {
|
||||
if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "email") {
|
||||
resp.ERROR(c, "系统已禁用邮箱注册!")
|
||||
return
|
||||
}
|
||||
// 检查邮箱后缀是否在白名单
|
||||
if len(h.App.SysConfig.EmailWhiteList) > 0 {
|
||||
if len(h.App.SysConfig.Base.EmailWhiteList) > 0 {
|
||||
inWhiteList := false
|
||||
for _, suffix := range h.App.SysConfig.EmailWhiteList {
|
||||
for _, suffix := range h.App.SysConfig.Base.EmailWhiteList {
|
||||
if strings.HasSuffix(data.Receiver, suffix) {
|
||||
inWhiteList = true
|
||||
break
|
||||
@@ -92,7 +99,7 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
}
|
||||
err = h.smtp.SendVerifyCode(data.Receiver, code)
|
||||
} else {
|
||||
if !utils.Contains(h.App.SysConfig.RegisterWays, "mobile") {
|
||||
if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "mobile") {
|
||||
resp.ERROR(c, "系统已禁用手机号注册!")
|
||||
return
|
||||
}
|
||||
|
||||
+76
-26
@@ -10,8 +10,10 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/suno"
|
||||
"geekai/store/model"
|
||||
@@ -26,20 +28,41 @@ import (
|
||||
|
||||
type SunoHandler struct {
|
||||
BaseHandler
|
||||
sunoService *suno.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
sunoService *suno.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService) *SunoHandler {
|
||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *SunoHandler {
|
||||
return &SunoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
sunoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
sunoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *SunoHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/suno/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.GET("play", h.Play)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("create", h.Create)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("detail", h.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +87,36 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceSuno,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.SunoPower {
|
||||
if user.Power < h.App.SysConfig.Base.SunoPower {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
@@ -79,9 +125,9 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
if data.SongId != "" && data.Type == 3 {
|
||||
var song model.SunoJob
|
||||
if err := h.DB.Where("song_id = ?", data.SongId).First(&song).Error; err == nil {
|
||||
data.Instrumental = song.Instrumental
|
||||
data.Model = song.ModelName
|
||||
data.Tags = song.Tags
|
||||
data.Instrumental = song.Params.Instrumental
|
||||
data.Model = song.Params.Model
|
||||
data.Tags = song.Params.Tags
|
||||
}
|
||||
// 拼接歌词
|
||||
var refSong model.SunoJob
|
||||
@@ -107,22 +153,26 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
|
||||
// 插入数据库
|
||||
job := model.SunoJob{
|
||||
UserId: uint(task.UserId),
|
||||
Prompt: data.Prompt,
|
||||
Instrumental: data.Instrumental,
|
||||
ModelName: data.Model,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Tags: data.Tags,
|
||||
Title: data.Title,
|
||||
Type: data.Type,
|
||||
RefSongId: data.RefSongId,
|
||||
RefTaskId: data.RefTaskId,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Power: h.App.SysConfig.SunoPower,
|
||||
SongId: utils.RandString(32),
|
||||
UserId: uint(task.UserId),
|
||||
Prompt: data.Prompt,
|
||||
Params: vo.SunoParam{
|
||||
Prompt: data.Prompt,
|
||||
Instrumental: data.Instrumental,
|
||||
Tags: data.Tags,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Lyrics: data.Lyrics,
|
||||
Model: data.Model,
|
||||
},
|
||||
Title: data.Title,
|
||||
Type: data.Type,
|
||||
RefSongId: data.RefSongId,
|
||||
RefTaskId: data.RefTaskId,
|
||||
Power: h.App.SysConfig.Base.SunoPower,
|
||||
SongId: utils.RandString(32),
|
||||
}
|
||||
if data.Lyrics != "" {
|
||||
job.Prompt = data.Lyrics
|
||||
job.Params.Prompt = data.Lyrics
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
if tx.Error != nil {
|
||||
@@ -137,8 +187,8 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: job.ModelName,
|
||||
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName),
|
||||
Model: job.Params.Model,
|
||||
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.Params.Model),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type TestHandler struct {
|
||||
App *core.AppServer
|
||||
db *gorm.DB
|
||||
snowflake *service.Snowflake
|
||||
js *payment.GeekPayService
|
||||
js *payment.EPayService
|
||||
}
|
||||
|
||||
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.GeekPayService) *TestHandler {
|
||||
return &TestHandler{db: db, snowflake: snowflake, js: js}
|
||||
func NewTestHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, js *payment.EPayService) *TestHandler {
|
||||
return &TestHandler{App: app, db: db, snowflake: snowflake, js: js}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *TestHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/test/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.Any("sse", h.PostTest, h.SseTest)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TestHandler) SseTest(c *gin.Context) {
|
||||
|
||||
+357
-256
@@ -8,8 +8,10 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store"
|
||||
@@ -20,8 +22,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
@@ -35,9 +35,11 @@ type UserHandler struct {
|
||||
searcher *xdb.Searcher
|
||||
redis *redis.Client
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
captcha *service.CaptchaService
|
||||
captchaService *service.CaptchaService
|
||||
userService *service.UserService
|
||||
wxLoginService *service.WxLoginService
|
||||
ipSearcher *xdb.Searcher
|
||||
wechatService *service.WxGzhService
|
||||
}
|
||||
|
||||
func NewUserHandler(
|
||||
@@ -48,15 +50,46 @@ func NewUserHandler(
|
||||
levelDB *store.LevelDB,
|
||||
captcha *service.CaptchaService,
|
||||
userService *service.UserService,
|
||||
licenseService *service.LicenseService) *UserHandler {
|
||||
wxLoginService *service.WxLoginService,
|
||||
wechatService *service.WxGzhService,
|
||||
ipSearcher *xdb.Searcher) *UserHandler {
|
||||
return &UserHandler{
|
||||
BaseHandler: BaseHandler{DB: db, App: app},
|
||||
searcher: searcher,
|
||||
redis: client,
|
||||
levelDB: levelDB,
|
||||
captcha: captcha,
|
||||
licenseService: licenseService,
|
||||
captchaService: captcha,
|
||||
userService: userService,
|
||||
wxLoginService: wxLoginService,
|
||||
wechatService: wechatService,
|
||||
ipSearcher: ipSearcher,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *UserHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/user/")
|
||||
|
||||
// 公开接口,不需要授权
|
||||
group.POST("register", h.Register)
|
||||
group.POST("login", h.Login)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
group.GET("login/qrcode", h.GetWxLoginQRCode)
|
||||
group.POST("login/callback", h.WxLoginCallback)
|
||||
group.GET("login/status", h.GetWxLoginState)
|
||||
group.GET("logout", h.Logout)
|
||||
group.POST("wxAuthLogin", h.WxAuthLogin)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("session", h.Session)
|
||||
group.GET("profile", h.Profile)
|
||||
group.POST("profile/update", h.ProfileUpdate)
|
||||
group.POST("password", h.UpdatePass)
|
||||
group.POST("bind/mobile", h.BindMobile)
|
||||
group.POST("bind/email", h.BindEmail)
|
||||
group.GET("signin", h.SignIn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,12 +113,13 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.EnabledVerify && data.RegWay == "username" {
|
||||
// 人机验证
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
@@ -99,14 +133,6 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
var key string
|
||||
if data.RegWay == "email" {
|
||||
@@ -125,30 +151,8 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邀请码
|
||||
inviteCode := model.InviteCode{}
|
||||
if data.InviteCode != "" {
|
||||
res := h.DB.Where("code = ?", data.InviteCode).First(&inviteCode)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "无效的邀请码")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
user := model.User{
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
ChatConfig: "{}",
|
||||
ChatModels: "{}",
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
}
|
||||
|
||||
// check if the username is existing
|
||||
user := model.User{Username: data.Username, Password: data.Password}
|
||||
var item model.User
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Mobile != "" {
|
||||
@@ -168,78 +172,19 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 被邀请人也获得赠送算力
|
||||
if data.InviteCode != "" {
|
||||
user.Power += h.App.SysConfig.InvitePower
|
||||
}
|
||||
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
defaultNickname := h.App.SysConfig.DefaultNickname
|
||||
if defaultNickname == "" {
|
||||
defaultNickname = "极客学长"
|
||||
}
|
||||
user.Nickname = fmt.Sprintf("%s@%d", defaultNickname, utils.RandomNumber(6))
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
user, err := h.createNewUser(user, data.InviteCode)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 记录邀请关系
|
||||
if data.InviteCode != "" {
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.InvitePower > 0 {
|
||||
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.InvitePower, model.PowerLog{
|
||||
Type: types.PowerInvite,
|
||||
Model: "Invite",
|
||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 添加邀请记录
|
||||
err := tx.Create(&model.InviteLog{
|
||||
InviterId: inviteCode.UserId,
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
InviteCode: inviteCode.Code,
|
||||
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.InvitePower),
|
||||
}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
|
||||
// 自动登录创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
key = fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
@@ -255,15 +200,12 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
verifyKey := fmt.Sprintf("users/verify/%s", data.Username)
|
||||
needVerify, err := h.redis.Get(c, verifyKey).Bool()
|
||||
|
||||
if h.App.SysConfig.EnabledVerify && needVerify {
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
@@ -274,54 +216,28 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
var user model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&user)
|
||||
if res.Error != nil {
|
||||
h.redis.Set(c, verifyKey, true, 0)
|
||||
resp.ERROR(c, "用户名不存在")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
if password != user.Password {
|
||||
h.redis.Set(c, verifyKey, true, 0)
|
||||
resp.ERROR(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Status == false {
|
||||
if !user.Status {
|
||||
resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最后登录时间和IP
|
||||
user.LastLoginIp = c.ClientIP()
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
h.DB.Model(&user).Updates(user)
|
||||
|
||||
h.DB.Create(&model.UserLoginLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
LoginIp: c.ClientIP(),
|
||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
||||
})
|
||||
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
sessionKey := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 移除登录行为验证码
|
||||
h.redis.Del(c, verifyKey)
|
||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
// Logout 注 销
|
||||
@@ -333,134 +249,181 @@ func (h *UserHandler) Logout(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// CLogin 第三方登录请求二维码
|
||||
func (h *UserHandler) CLogin(c *gin.Context) {
|
||||
returnURL := h.GetTrim(c, "return_url")
|
||||
var res types.BizVo
|
||||
apiURL := fmt.Sprintf("%s/api/clogin/request", h.App.Config.ApiConfig.ApiURL)
|
||||
r, err := req.C().R().SetBody(gin.H{"login_type": "wx", "return_url": returnURL}).
|
||||
SetHeader("AppId", h.App.Config.ApiConfig.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
// GetWxLoginQRCode 获取微信登录二维码URL
|
||||
func (h *UserHandler) GetWxLoginQRCode(c *gin.Context) {
|
||||
if !h.wxLoginService.GetConfig().Enabled {
|
||||
resp.ERROR(c, "微信登录功能未启用")
|
||||
return
|
||||
}
|
||||
|
||||
if h.wxLoginService.GetConfig().ApiKey == "" {
|
||||
resp.ERROR(c, "微信登录服务令牌未配置")
|
||||
return
|
||||
}
|
||||
|
||||
state := utils.RandString(32)
|
||||
qrCodeURL, err := h.wxLoginService.GetLoginQrCodeUrl(state)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, "error with login http status: "+r.Status)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, "error with http response: "+res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, res.Data)
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"url": qrCodeURL,
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
|
||||
// CLoginCallback 第三方登录回调
|
||||
func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
||||
loginType := c.Query("login_type")
|
||||
code := c.Query("code")
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := c.Query("action")
|
||||
// 查询微信登录状态
|
||||
func (h *UserHandler) GetWxLoginState(c *gin.Context) {
|
||||
state := c.Query("state")
|
||||
if state == "" {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var res types.BizVo
|
||||
apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL)
|
||||
r, err := req.C().R().SetBody(gin.H{"login_type": loginType, "code": code}).
|
||||
SetHeader("AppId", h.App.Config.ApiConfig.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
status, err := h.wxLoginService.GetLoginStatus(state)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, "error with login http status: "+r.Status)
|
||||
|
||||
if status.Status != service.LoginStatusSuccess {
|
||||
resp.SUCCESS(c, status)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, "error with http response: "+res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
// login successfully
|
||||
data := res.Data.(map[string]interface{})
|
||||
// 登录成功
|
||||
var user model.User
|
||||
if action == "bind" && userId > 0 {
|
||||
err = h.DB.Where("openid", data["openid"]).First(&user).Error
|
||||
if err == nil {
|
||||
resp.ERROR(c, "该微信已经绑定其他账号,请先解绑")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.DB.Where("id", userId).First(&user).Error
|
||||
h.DB.Where("openid = ?", status.OpenID).First(&user)
|
||||
if user.Id == 0 {
|
||||
// 创建新用户
|
||||
user, err = h.createNewUser(model.User{OpenId: status.OpenID}, "")
|
||||
if err != nil {
|
||||
resp.ERROR(c, "绑定用户不存在")
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新用户信息失败,"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": ""})
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
session := gin.H{}
|
||||
tx := h.DB.Where("openid", data["openid"]).First(&user)
|
||||
if tx.Error != nil {
|
||||
// create new user
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
status.Status = service.LoginStatusExpired
|
||||
h.wxLoginService.SetLoginStatus(state, *status)
|
||||
|
||||
salt := utils.RandString(8)
|
||||
password := fmt.Sprintf("%d", utils.RandomNumber(8))
|
||||
user = model.User{
|
||||
Username: fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)),
|
||||
Password: utils.GenPassword(password, salt),
|
||||
Avatar: fmt.Sprintf("%s", data["avatar"]),
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
OpenId: fmt.Sprintf("%s", data["openid"]),
|
||||
Nickname: fmt.Sprintf("%s", data["nickname"]),
|
||||
}
|
||||
status.Status = service.LoginStatusSuccess
|
||||
status.Token = token
|
||||
resp.SUCCESS(c, status)
|
||||
}
|
||||
|
||||
tx = h.DB.Create(&user)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, "保存数据失败")
|
||||
logger.Error(tx.Error)
|
||||
return
|
||||
// createNewUser 创建新用户
|
||||
func (h *UserHandler) createNewUser(user model.User, code string) (model.User, error) {
|
||||
if user.OpenId != "" {
|
||||
user.Platform = "wechat"
|
||||
// 如果未设置昵称,则生成默认昵称
|
||||
if user.Nickname == "" {
|
||||
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
|
||||
}
|
||||
// 如果未设置用户名,则生成默认用户名
|
||||
if user.Username == "" {
|
||||
user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8))
|
||||
}
|
||||
// 如果未设置密码,则生成默认密码
|
||||
if user.Password == "" {
|
||||
user.Password = "geekai123"
|
||||
}
|
||||
} else {
|
||||
if user.Nickname == "" {
|
||||
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
}
|
||||
if user.Username == "" || user.Password == "" {
|
||||
return user, fmt.Errorf("用户名或密码不能为空")
|
||||
}
|
||||
session["username"] = user.Username
|
||||
session["password"] = password
|
||||
} else { // login directly
|
||||
// 更新最后登录时间和IP
|
||||
user.LastLoginIp = c.ClientIP()
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
h.DB.Model(&user).Updates(user)
|
||||
|
||||
h.DB.Create(&model.UserLoginLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
LoginIp: c.ClientIP(),
|
||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
||||
})
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
user.Salt = salt
|
||||
user.Password = utils.GenPassword(user.Password, salt)
|
||||
// 如果未设置头像,则使用默认头像
|
||||
if user.Avatar == "" {
|
||||
user.Avatar = "/images/avatar/user.png"
|
||||
}
|
||||
user.Status = true
|
||||
user.ChatConfig = "{}"
|
||||
user.ChatModels = "{}"
|
||||
user.Power = h.App.SysConfig.Base.InitPower
|
||||
|
||||
// 创建用户
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return user, err
|
||||
}
|
||||
|
||||
// 记录邀请关系
|
||||
if code != "" {
|
||||
var inviteCode model.InviteCode
|
||||
err := h.DB.Where("code = ?", code).First(&inviteCode).Error
|
||||
if err != nil {
|
||||
logger.Error("无效的邀请码:", err.Error())
|
||||
tx.Rollback()
|
||||
return user, fmt.Errorf("无效的邀请码")
|
||||
}
|
||||
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.Base.InvitePower > 0 {
|
||||
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
|
||||
Type: types.PowerInvite,
|
||||
Model: "Invite",
|
||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.Base.InvitePower, inviteCode.Code, user.Username),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return user, err
|
||||
}
|
||||
|
||||
// 添加邀请记录
|
||||
err = tx.Create(&model.InviteLog{
|
||||
InviterId: inviteCode.UserId,
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
InviteCode: inviteCode.Code,
|
||||
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.Base.InvitePower),
|
||||
}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// doLogin 执行登录操作
|
||||
func (h *UserHandler) doLogin(user *model.User, ip string) (string, error) {
|
||||
// 更新最后登录时间和IP
|
||||
user.LastLoginIp = ip
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
err := h.DB.Model(user).Updates(user).Error
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to update user: %v", err)
|
||||
}
|
||||
|
||||
// 记录登录日志
|
||||
h.DB.Create(&model.UserLoginLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
LoginIp: ip,
|
||||
LoginAddress: utils.Ip2Region(h.ipSearcher, ip),
|
||||
})
|
||||
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
@@ -468,17 +431,42 @@ func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
return
|
||||
return "", fmt.Errorf("failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
// 保存到 redis
|
||||
key := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
sessionKey := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err = h.redis.Set(context.Background(), sessionKey, tokenString, 0).Result(); err != nil {
|
||||
return "", fmt.Errorf("error with save token: %v", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// WxLoginCallback 微信登录回调处理
|
||||
func (h *UserHandler) WxLoginCallback(c *gin.Context) {
|
||||
var data struct {
|
||||
OpenID string `json:"openid"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
session["token"] = tokenString
|
||||
resp.SUCCESS(c, session)
|
||||
|
||||
if data.OpenID == "" || data.State == "" {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置登录状态
|
||||
status := service.LoginStatus{
|
||||
Status: service.LoginStatusSuccess,
|
||||
OpenID: data.OpenID,
|
||||
}
|
||||
h.wxLoginService.SetLoginStatus(data.State, status)
|
||||
|
||||
resp.SUCCESS(c, status)
|
||||
}
|
||||
|
||||
// Session 获取/验证会话
|
||||
@@ -500,6 +488,20 @@ func (h *UserHandler) Session(c *gin.Context) {
|
||||
h.DB.Model(&user).UpdateColumn("vip", false)
|
||||
}
|
||||
userVo.Id = user.Id
|
||||
// 工作区应用 ID 列表(历史可能为 key 数组,仅解析数字 ID)
|
||||
if user.ChatRoles != "" {
|
||||
var raw []interface{}
|
||||
if utils.JsonDecode(user.ChatRoles, &raw) == nil {
|
||||
for _, v := range raw {
|
||||
if n, ok := v.(float64); ok && n >= 0 {
|
||||
userVo.ChatRoles = append(userVo.ChatRoles, uint(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if userVo.ChatRoles == nil {
|
||||
userVo.ChatRoles = []uint{}
|
||||
}
|
||||
resp.SUCCESS(c, userVo)
|
||||
|
||||
}
|
||||
@@ -512,6 +514,7 @@ type userProfile struct {
|
||||
Power int `json:"power"`
|
||||
ExpiredTime int64 `json:"expired_time"`
|
||||
Vip bool `json:"vip"`
|
||||
GemIds []uint `json:"gem_ids"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) Profile(c *gin.Context) {
|
||||
@@ -531,6 +534,19 @@ func (h *UserHandler) Profile(c *gin.Context) {
|
||||
}
|
||||
|
||||
profile.Id = user.Id
|
||||
if user.GemIds != "" {
|
||||
var raw []interface{}
|
||||
if utils.JsonDecode(user.GemIds, &raw) == nil {
|
||||
for _, v := range raw {
|
||||
if n, ok := v.(float64); ok {
|
||||
profile.GemIds = append(profile.GemIds, uint(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if profile.GemIds == nil {
|
||||
profile.GemIds = []uint{}
|
||||
}
|
||||
resp.SUCCESS(c, profile)
|
||||
}
|
||||
|
||||
@@ -549,6 +565,12 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
|
||||
h.DB.First(&user, user.Id)
|
||||
user.Avatar = data.Avatar
|
||||
user.Nickname = data.Nickname
|
||||
if data.GemIds != nil {
|
||||
if len(data.GemIds) > 8 {
|
||||
data.GemIds = data.GemIds[:8]
|
||||
}
|
||||
user.GemIds = utils.JsonEncode(data.GemIds)
|
||||
}
|
||||
res := h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新用户信息失败")
|
||||
@@ -613,13 +635,14 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
var key string
|
||||
if data.Type == "email" {
|
||||
switch data.Type {
|
||||
case "email":
|
||||
session = session.Where("email", data.Email)
|
||||
key = CodeStorePrefix + data.Email
|
||||
} else if data.Type == "mobile" {
|
||||
case "mobile":
|
||||
session = session.Where("mobile", data.Mobile)
|
||||
key = CodeStorePrefix + data.Mobile
|
||||
} else {
|
||||
default:
|
||||
resp.ERROR(c, "验证类别错误")
|
||||
return
|
||||
}
|
||||
@@ -742,12 +765,90 @@ func (h *UserHandler) SignIn(c *gin.Context) {
|
||||
|
||||
// 签到
|
||||
h.levelDB.Put(key, true)
|
||||
if h.App.SysConfig.DailyPower > 0 {
|
||||
h.userService.IncreasePower(userId, h.App.SysConfig.DailyPower, model.PowerLog{
|
||||
if h.App.SysConfig.Base.DailyPower > 0 {
|
||||
h.userService.IncreasePower(userId, h.App.SysConfig.Base.DailyPower, model.PowerLog{
|
||||
Type: types.PowerSignIn,
|
||||
Model: "SignIn",
|
||||
Remark: fmt.Sprintf("每日签到奖励,金额:%d", h.App.SysConfig.DailyPower),
|
||||
Remark: fmt.Sprintf("每日签到奖励,金额:%d", h.App.SysConfig.Base.DailyPower),
|
||||
})
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// 微信公众号 小程序授权登录
|
||||
func (h *UserHandler) WxAuthLogin(c *gin.Context) {
|
||||
var data struct {
|
||||
Code string `json:"code"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 根据 code 获取 openid
|
||||
openID, accessToken, err := h.wechatService.GetOpenIDByCode(data.Code)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取微信用户昵称头像
|
||||
userInfo, err := h.wechatService.GetUserInfo(accessToken, openID)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
nickname := userInfo["nickname"].(string)
|
||||
headimgurl := userInfo["headimgurl"].(string)
|
||||
|
||||
// 查询用户是否存在
|
||||
var user model.User
|
||||
h.DB.Where("openid = ?", openID).First(&user)
|
||||
if user.Id > 0 {
|
||||
// 用户存在,更新用户信息并登录
|
||||
user.Nickname = nickname
|
||||
user.Avatar = headimgurl
|
||||
if err := h.DB.Save(&user).Error; err != nil {
|
||||
resp.ERROR(c, "更新用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
|
||||
return
|
||||
}
|
||||
|
||||
// 用户不存在,创建新用户
|
||||
user = model.User{
|
||||
OpenId: openID,
|
||||
Nickname: nickname,
|
||||
Avatar: headimgurl,
|
||||
}
|
||||
|
||||
// 被邀请人也获得赠送算力
|
||||
if data.InviteCode != "" {
|
||||
user.Power = h.App.SysConfig.Base.InitPower * 2
|
||||
}
|
||||
|
||||
user, err = h.createNewUser(user, data.InviteCode)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 自动登录
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
+173
-159
@@ -10,15 +10,16 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/video"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -26,171 +27,135 @@ import (
|
||||
|
||||
type VideoHandler struct {
|
||||
BaseHandler
|
||||
videoService *video.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
videoService *video.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService) *VideoHandler {
|
||||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *VideoHandler {
|
||||
return &VideoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
videoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
videoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *VideoHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/video/")
|
||||
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
FirstFrameImg string `json:"first_frame_img,omitempty"`
|
||||
EndFrameImg string `json:"end_frame_img,omitempty"`
|
||||
ExpandPrompt bool `json:"expand_prompt,omitempty"`
|
||||
Loop bool `json:"loop,omitempty"`
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("create", h.Create)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.GET("power-config", h.GetPowerConfig) // 获取算力配置
|
||||
group.GET("power-by-key", h.GetPowerByPriceKey) // 根据 priceKey 获取算力
|
||||
}
|
||||
}
|
||||
|
||||
type VideoTaskRequest struct {
|
||||
Provider string `json:"provider"` // 服务提供商(不带版本号:veo, sora)
|
||||
Model string `json:"model"` // 模型标识(带版本号:veo-2.0, sora-2.0)
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
Params map[string]any `json:"params"` // 模型特定参数
|
||||
PriceKey string `json:"price_key"` // 价格键(如 "fixed", "5_720P" 等)
|
||||
}
|
||||
|
||||
// Create 统一的创建视频任务接口
|
||||
func (h *VideoHandler) Create(c *gin.Context) {
|
||||
var data VideoTaskRequest
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 检查 Prompt 长度
|
||||
|
||||
// 验证必填字段
|
||||
if data.Provider == "" {
|
||||
resp.ERROR(c, "provider 不能为空")
|
||||
return
|
||||
}
|
||||
if data.Model == "" {
|
||||
resp.ERROR(c, "model 不能为空")
|
||||
return
|
||||
}
|
||||
if data.Prompt == "" {
|
||||
resp.ERROR(c, "prompt is needed")
|
||||
resp.ERROR(c, "prompt 不能为空")
|
||||
return
|
||||
}
|
||||
if data.PriceKey == "" {
|
||||
resp.ERROR(c, "price_key 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 文本审查
|
||||
if h.App.SysConfig.Moderation.Enable {
|
||||
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||
if err != nil {
|
||||
logger.Error("failed to moderate content: ", err)
|
||||
}
|
||||
if moderationResult.Flagged {
|
||||
// 记录违规内容
|
||||
moderation := model.Moderation{
|
||||
UserId: h.GetLoginUserId(c),
|
||||
Source: types.ModerationSourceVideo,
|
||||
Input: data.Prompt,
|
||||
Result: utils.JsonEncode(moderationResult),
|
||||
}
|
||||
err = h.DB.Create(&moderation).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save moderation: ", err)
|
||||
}
|
||||
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.LumaPower {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
|
||||
userId := int(h.GetLoginUserId(c))
|
||||
params := types.LumaVideoParams{
|
||||
PromptOptimize: data.ExpandPrompt,
|
||||
Loop: data.Loop,
|
||||
StartImgURL: data.FirstFrameImg,
|
||||
EndImgURL: data.EndFrameImg,
|
||||
}
|
||||
task := types.VideoTask{
|
||||
UserId: userId,
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
}
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: uint(userId),
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
Power: h.App.SysConfig.LumaPower,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, tx.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
task.Id = job.Id
|
||||
h.videoService.PushTask(task)
|
||||
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "luma",
|
||||
Remark: fmt.Sprintf("Luma 文生视频,任务ID:%d", job.Id),
|
||||
})
|
||||
// 计算算力
|
||||
power, err := video.CalculatePower(h.DB, data.Model, data.PriceKey)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *VideoHandler) KeLingCreate(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
Channel string `json:"channel"`
|
||||
TaskType string `json:"task_type"` // 任务类型: text2video/image2video
|
||||
Model string `json:"model"` // 模型: kling-v1-5,kling-v1-6
|
||||
Prompt string `json:"prompt"` // 视频描述
|
||||
NegPrompt string `json:"negative_prompt"` // 负面提示词
|
||||
CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1)
|
||||
Mode string `json:"mode"` // 生成模式: std/pro
|
||||
AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1
|
||||
Duration string `json:"duration"` // 视频时长: 5/10
|
||||
CameraControl types.CameraControl `json:"camera_control"` // 摄像机控制
|
||||
Image string `json:"image"` // 参考图片URL(image2video)
|
||||
ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video)
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 计算当前任务所需算力
|
||||
key := fmt.Sprintf("%s_%s_%s", data.Model, data.Mode, data.Duration)
|
||||
power := h.App.SysConfig.KeLingPowers[key]
|
||||
if power == 0 {
|
||||
resp.ERROR(c, "当前模型暂不支持")
|
||||
return
|
||||
}
|
||||
// 检查算力是否充足
|
||||
if user.Power < power {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
|
||||
if data.Prompt == "" {
|
||||
resp.ERROR(c, "prompt is needed")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建任务
|
||||
userId := int(h.GetLoginUserId(c))
|
||||
params := types.KeLingVideoParams{
|
||||
TaskType: data.TaskType,
|
||||
Model: data.Model,
|
||||
Prompt: data.Prompt,
|
||||
NegPrompt: data.NegPrompt,
|
||||
CfgScale: data.CfgScale,
|
||||
Mode: data.Mode,
|
||||
AspectRatio: data.AspectRatio,
|
||||
Duration: data.Duration,
|
||||
CameraControl: data.CameraControl,
|
||||
Image: data.Image,
|
||||
ImageTail: data.ImageTail,
|
||||
}
|
||||
task := types.VideoTask{
|
||||
UserId: userId,
|
||||
Type: types.VideoKeLing,
|
||||
Type: data.Provider, // provider 作为 type
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
Channel: data.Channel,
|
||||
Params: data.Params,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: uint(userId),
|
||||
Type: types.VideoKeLing,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
UserId: uint(userId),
|
||||
Type: data.Provider,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
Params: utils.JsonEncode(task),
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
if tx.Error != nil {
|
||||
@@ -202,17 +167,52 @@ func (h *VideoHandler) KeLingCreate(c *gin.Context) {
|
||||
task.Id = job.Id
|
||||
h.videoService.PushTask(task)
|
||||
|
||||
// update user's power
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "keling",
|
||||
Remark: fmt.Sprintf("keling 文生视频,任务ID:%d", job.Id),
|
||||
Model: data.Provider,
|
||||
Remark: fmt.Sprintf("%s 视频生成,任务ID:%d", data.Provider, job.Id),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
|
||||
resp.SUCCESS(c, gin.H{"job_id": job.Id})
|
||||
}
|
||||
|
||||
// GetPowerConfig 获取算力配置
|
||||
func (h *VideoHandler) GetPowerConfig(c *gin.Context) {
|
||||
config, err := video.GetVideoConfig(h.DB)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, config.VideoPowers)
|
||||
}
|
||||
|
||||
// GetPowerByPriceKey 根据 modelKey 和 priceKey 获取算力值
|
||||
func (h *VideoHandler) GetPowerByPriceKey(c *gin.Context) {
|
||||
modelKey := c.Query("model_key")
|
||||
priceKey := c.Query("price_key")
|
||||
|
||||
if modelKey == "" {
|
||||
resp.ERROR(c, "model_key 不能为空")
|
||||
return
|
||||
}
|
||||
if priceKey == "" {
|
||||
resp.ERROR(c, "price_key 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
power, err := video.CalculatePower(h.DB, modelKey, priceKey)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"power": power})
|
||||
}
|
||||
|
||||
func (h *VideoHandler) List(c *gin.Context) {
|
||||
@@ -226,7 +226,7 @@ func (h *VideoHandler) List(c *gin.Context) {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
if all {
|
||||
session = session.Where("publish", 0).Where("progress", 100)
|
||||
session = session.Where("publish", 0).Where("status", types.VideoStatusSuccess)
|
||||
} else {
|
||||
session = session.Where("user_id", userId)
|
||||
}
|
||||
@@ -254,36 +254,51 @@ func (h *VideoHandler) List(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
item.CreatedAt = v.CreatedAt.Unix()
|
||||
if item.VideoURL == "" {
|
||||
item.VideoURL = v.WaterURL
|
||||
}
|
||||
// 解析任务详情
|
||||
if item.Type == types.VideoKeLing {
|
||||
// 解析任务详情(用于前端展示标签)
|
||||
if v.Params != "" {
|
||||
task := types.VideoTask{}
|
||||
err = utils.JsonDecode(v.TaskInfo, &task)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var params types.KeLingVideoParams
|
||||
err = utils.JsonDecode(utils.JsonEncode(task.Params), ¶ms)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item.RawData = map[string]interface{}{
|
||||
"task_type": params.TaskType,
|
||||
"model": params.Model,
|
||||
"cfg_scale": params.CfgScale,
|
||||
"mode": params.Mode,
|
||||
"aspect_ratio": params.AspectRatio,
|
||||
"duration": params.Duration,
|
||||
"model_name": fmt.Sprintf("%s_%s_%s", params.Model, params.Mode, params.Duration),
|
||||
}
|
||||
|
||||
// 如果视频URL不为空,则设置为生成成功
|
||||
if item.VideoURL != "" {
|
||||
item.Progress = 100
|
||||
if err := utils.JsonDecode(v.Params, &task); err == nil {
|
||||
// 默认从 params map 中提取常用字段
|
||||
if paramsMap, ok := task.Params.(map[string]any); ok {
|
||||
if item.Params == nil {
|
||||
item.Params = make(map[string]any)
|
||||
}
|
||||
if _, ok := item.Params["task_type"]; !ok {
|
||||
if taskType, ok := paramsMap["task_type"]; ok {
|
||||
item.Params["task_type"] = taskType
|
||||
}
|
||||
}
|
||||
if _, ok := item.Params["model"]; !ok {
|
||||
if modelKey, ok := paramsMap["model"]; ok {
|
||||
item.Params["model"] = modelKey
|
||||
}
|
||||
}
|
||||
if _, ok := item.Params["duration"]; !ok {
|
||||
if duration, ok := paramsMap["duration"]; ok {
|
||||
item.Params["duration"] = duration
|
||||
}
|
||||
}
|
||||
if _, ok := item.Params["size"]; !ok {
|
||||
if size, ok := paramsMap["size"]; ok {
|
||||
item.Params["size"] = size
|
||||
} else if size, ok := paramsMap["resolution"].(string); ok {
|
||||
item.Params["size"] = size
|
||||
}
|
||||
}
|
||||
if _, ok := item.Params["mode"]; !ok {
|
||||
if mode, ok := paramsMap["mode"]; ok {
|
||||
item.Params["mode"] = mode
|
||||
}
|
||||
}
|
||||
if _, ok := item.Params["sound"]; !ok {
|
||||
if sound, ok := paramsMap["sound"]; ok {
|
||||
item.Params["sound"] = sound
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
@@ -299,9 +314,9 @@ func (h *VideoHandler) Remove(c *gin.Context) {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 只有失败或者超时的任务才能删除
|
||||
if !(job.Progress == service.FailTaskProgress || time.Now().After(job.CreatedAt.Add(time.Minute*30))) {
|
||||
resp.ERROR(c, "只有失败和超时(30分钟)的任务才能删除!")
|
||||
// 只有失败的任务才能删除
|
||||
if job.Status != types.VideoStatusFailed {
|
||||
resp.ERROR(c, "只有失败的任务才能删除!")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -313,7 +328,6 @@ func (h *VideoHandler) Remove(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
|
||||
|
||||
resp.SUCCESS(c)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"geekai/core"
|
||||
"geekai/utils/resp"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type WxGzhHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewWxGzhHandler(server *core.AppServer) *WxGzhHandler {
|
||||
return &WxGzhHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WxGzhHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/wx/")
|
||||
group.GET("verify", h.WechatVerify)
|
||||
}
|
||||
|
||||
// 处理微信服务器验证请求
|
||||
func (h *WxGzhHandler) WechatVerify(c *gin.Context) {
|
||||
logger.Info("WechatVerify")
|
||||
// 只处理 GET 请求
|
||||
if c.Request.Method != "GET" {
|
||||
resp.ERROR(c, "Method Not Allowed")
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 URL 参数
|
||||
signature := c.Query("signature")
|
||||
timestamp := c.Query("timestamp")
|
||||
nonce := c.Query("nonce")
|
||||
echostr := c.Query("echostr")
|
||||
|
||||
// 验证参数完整性
|
||||
if signature == "" || timestamp == "" || nonce == "" || echostr == "" {
|
||||
log.Println("Missing parameters")
|
||||
resp.ERROR(c, "Missing parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if validateSignature(signature, h.App.SysConfig.WxGzh.Token, timestamp, nonce) {
|
||||
// 验证成功,返回 echostr(必须是纯文本)
|
||||
c.String(http.StatusOK, echostr)
|
||||
log.Println("Token verification success")
|
||||
} else {
|
||||
// 验证失败
|
||||
resp.ERROR(c, "Forbidden: Invalid signature")
|
||||
log.Println("Token verification failed")
|
||||
}
|
||||
}
|
||||
|
||||
func validateSignature(signature, token, timestamp, nonce string) bool {
|
||||
// 1. 将 token、timestamp、nonce 按字典序排序
|
||||
strs := []string{token, timestamp, nonce}
|
||||
sort.Strings(strs)
|
||||
|
||||
// 2. 拼接字符串
|
||||
joined := strings.Join(strs, "")
|
||||
|
||||
// 3. 计算 SHA1 哈希
|
||||
hash := sha1.New()
|
||||
hash.Write([]byte(joined))
|
||||
hashed := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
// 4. 与 signature 比对
|
||||
return hashed == signature
|
||||
}
|
||||
|
||||
// 创建微信菜单
|
||||
func (h *WxGzhHandler) CreateMenu(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, "创建菜单成功")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package logger
|
||||
package log
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
+165
-280
@@ -14,22 +14,24 @@ import (
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/handler/admin"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/log"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/image"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sd"
|
||||
"geekai/service/ppt"
|
||||
"geekai/service/sms"
|
||||
"geekai/service/sora"
|
||||
"geekai/service/suno"
|
||||
"geekai/service/video"
|
||||
"geekai/store"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -40,7 +42,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
var logger = log.GetLogger()
|
||||
|
||||
//go:embed res
|
||||
var xdbFS embed.FS
|
||||
@@ -70,34 +72,35 @@ func main() {
|
||||
if configFile == "" {
|
||||
configFile = "config.toml"
|
||||
}
|
||||
debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG"))
|
||||
logger.Info("Loading config file: ", configFile)
|
||||
if !debug {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("Panic Error:", err)
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("Panic Error:", err)
|
||||
// 打印堆栈信息
|
||||
if os.Getenv("GEEKAI_DEBUG") == "true" {
|
||||
debug.PrintStack()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
app := fx.New(
|
||||
// 初始化配置应用配置
|
||||
fx.Provide(func() *types.AppConfig {
|
||||
config, err := core.LoadConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Fatal(err)
|
||||
}
|
||||
config.Path = configFile
|
||||
if debug {
|
||||
_ = core.SaveConfig(config)
|
||||
}
|
||||
return config
|
||||
}),
|
||||
// 创建应用服务
|
||||
fx.Provide(core.NewServer),
|
||||
// 初始化
|
||||
fx.Invoke(func(s *core.AppServer, client *redis.Client) {
|
||||
s.Init(debug, client)
|
||||
s.Init(client)
|
||||
}),
|
||||
fx.Provide(func(db *gorm.DB) *types.SystemConfig {
|
||||
return core.LoadSystemConfig(db)
|
||||
}),
|
||||
|
||||
// 初始化数据库
|
||||
@@ -125,22 +128,36 @@ func main() {
|
||||
}),
|
||||
|
||||
// 创建控制器
|
||||
fx.Provide(handler.NewChatRoleHandler),
|
||||
fx.Provide(handler.NewChatAppHandler),
|
||||
fx.Provide(handler.NewUserHandler),
|
||||
fx.Provide(handler.NewChatHandler),
|
||||
fx.Provide(handler.NewNetHandler),
|
||||
fx.Provide(handler.NewSmsHandler),
|
||||
fx.Provide(handler.NewRedeemHandler),
|
||||
fx.Provide(handler.NewCaptchaHandler),
|
||||
fx.Provide(func(db *gorm.DB, userService *service.UserService, uploadManager *oss.UploaderManager) *ppt.PptService {
|
||||
return ppt.NewPptService(db, userService, uploadManager)
|
||||
}),
|
||||
fx.Provide(handler.NewPPTTaskHandler),
|
||||
fx.Invoke(func(ppt *ppt.PptService) {
|
||||
ppt.RecoverStaleProcessingTasks()
|
||||
}),
|
||||
fx.Provide(handler.NewMidJourneyHandler),
|
||||
fx.Provide(handler.NewChatModelHandler),
|
||||
fx.Provide(handler.NewSdJobHandler),
|
||||
fx.Provide(handler.NewPaymentHandler),
|
||||
fx.Provide(handler.NewOrderHandler),
|
||||
fx.Provide(handler.NewProductHandler),
|
||||
fx.Provide(handler.NewConfigHandler),
|
||||
fx.Provide(handler.NewPowerLogHandler),
|
||||
fx.Provide(handler.NewJimengHandler),
|
||||
fx.Provide(service.NewWxGzhService),
|
||||
fx.Provide(handler.NewWxGzhHandler),
|
||||
fx.Provide(service.NewMigrationService),
|
||||
fx.Invoke(func(migrationService *service.MigrationService) {
|
||||
migrationService.StartMigrate()
|
||||
}),
|
||||
|
||||
// 管理后台控制器
|
||||
fx.Provide(admin.NewConfigHandler),
|
||||
fx.Provide(admin.NewAdminHandler),
|
||||
fx.Provide(admin.NewApiKeyHandler),
|
||||
@@ -151,28 +168,20 @@ func main() {
|
||||
fx.Provide(admin.NewChatModelHandler),
|
||||
fx.Provide(admin.NewProductHandler),
|
||||
fx.Provide(admin.NewOrderHandler),
|
||||
fx.Provide(admin.NewChatHandler),
|
||||
fx.Provide(admin.NewPowerLogHandler),
|
||||
|
||||
// 创建服务
|
||||
fx.Provide(sms.NewSendServiceManager),
|
||||
fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
|
||||
return service.NewCaptchaService(config.ApiConfig)
|
||||
}),
|
||||
fx.Provide(oss.NewUploaderManager),
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(s *dalle.Service) {
|
||||
s.Run()
|
||||
s.DownloadImages()
|
||||
s.CheckTaskStatus()
|
||||
}),
|
||||
fx.Provide(admin.NewAdminJimengHandler),
|
||||
fx.Provide(admin.NewVideoHandler),
|
||||
fx.Provide(admin.NewSunoHandler),
|
||||
fx.Provide(admin.NewPPTHandler),
|
||||
|
||||
// 邮件服务
|
||||
fx.Provide(service.NewSmtpService),
|
||||
// License 服务
|
||||
fx.Provide(service.NewLicenseService),
|
||||
fx.Invoke(func(licenseService *service.LicenseService) {
|
||||
licenseService.SyncLicense()
|
||||
// Image 服务
|
||||
fx.Provide(image.NewService),
|
||||
fx.Invoke(func(s *image.Service) {
|
||||
s.Run()
|
||||
s.DownloadImages()
|
||||
s.CheckTaskStatus()
|
||||
}),
|
||||
|
||||
// MidJourney service pool
|
||||
@@ -184,12 +193,8 @@ func main() {
|
||||
s.DownloadImages()
|
||||
}),
|
||||
|
||||
// Stable Diffusion 机器人
|
||||
fx.Provide(sd.NewService),
|
||||
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
|
||||
s.Run()
|
||||
s.CheckTaskStatus()
|
||||
}),
|
||||
// Sora service
|
||||
fx.Provide(sora.NewSoraService),
|
||||
|
||||
fx.Provide(suno.NewService),
|
||||
fx.Invoke(func(s *suno.Service) {
|
||||
@@ -203,325 +208,217 @@ func main() {
|
||||
s.SyncTaskProgress()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
fx.Provide(service.NewUserService),
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
fx.Provide(payment.NewHuPiPay),
|
||||
fx.Provide(payment.NewJPayService),
|
||||
fx.Provide(payment.NewWechatService),
|
||||
|
||||
// 即梦AI 服务
|
||||
fx.Provide(jimeng.NewClient),
|
||||
fx.Provide(jimeng.NewService),
|
||||
fx.Invoke(func(service *jimeng.Service) {
|
||||
service.Start()
|
||||
}),
|
||||
|
||||
fx.Provide(service.NewSnowflake),
|
||||
fx.Provide(service.NewXXLJobExecutor),
|
||||
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
|
||||
if config.XXLConfig.Enabled {
|
||||
go func() {
|
||||
log.Fatal(exec.Run())
|
||||
}()
|
||||
}
|
||||
|
||||
// 创建短信服务
|
||||
fx.Provide(sms.NewAliYunSmsService),
|
||||
fx.Provide(sms.NewBaoSmsService),
|
||||
fx.Provide(sms.NewTencentSmsService),
|
||||
fx.Provide(sms.NewSmsManager),
|
||||
fx.Provide(service.NewCaptchaService),
|
||||
fx.Provide(service.NewWxLoginService),
|
||||
|
||||
// 支付服务
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
fx.Provide(payment.NewEPayService),
|
||||
fx.Provide(payment.NewWxpayService),
|
||||
|
||||
// 文件上传服务
|
||||
fx.Provide(oss.NewLocalStorage),
|
||||
fx.Provide(oss.NewMiniOss),
|
||||
fx.Provide(oss.NewQiNiuOss),
|
||||
fx.Provide(oss.NewAliYunOss),
|
||||
fx.Provide(oss.NewTencentOss),
|
||||
fx.Provide(oss.NewUploaderManager),
|
||||
|
||||
// 用户服务
|
||||
fx.Provide(service.NewUserService),
|
||||
|
||||
// 文本审查服务
|
||||
fx.Provide(moderation.NewGiteeAIModeration),
|
||||
fx.Provide(moderation.NewBaiduAIModeration),
|
||||
fx.Provide(moderation.NewTencentAIModeration),
|
||||
fx.Provide(moderation.NewServiceManager),
|
||||
fx.Provide(admin.NewModerationHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ModerationHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 注册路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
|
||||
group := s.Engine.Group("/api/app/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("list/user", h.ListByUser)
|
||||
group.POST("update", h.UpdateRole)
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
|
||||
group := s.Engine.Group("/api/user/")
|
||||
group.POST("register", h.Register)
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
group.GET("session", h.Session)
|
||||
group.GET("profile", h.Profile)
|
||||
group.POST("profile/update", h.ProfileUpdate)
|
||||
group.POST("password", h.UpdatePass)
|
||||
group.POST("bind/mobile", h.BindMobile)
|
||||
group.POST("bind/email", h.BindEmail)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
group.GET("clogin", h.CLogin)
|
||||
group.GET("clogin/callback", h.CLoginCallback)
|
||||
group.GET("signin", h.SignIn)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
|
||||
group := s.Engine.Group("/api/chat/")
|
||||
group.Any("message", h.Chat)
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("history", h.History)
|
||||
group.GET("clear", h.Clear)
|
||||
group.POST("tokens", h.Tokens)
|
||||
group.GET("stop", h.StopGenerate)
|
||||
group.POST("tts", h.TextToSpeech)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
|
||||
s.Engine.POST("/api/upload", h.Upload)
|
||||
s.Engine.POST("/api/upload/list", h.List)
|
||||
s.Engine.GET("/api/upload/remove", h.Remove)
|
||||
s.Engine.GET("/api/download", h.Download)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
|
||||
group := s.Engine.Group("/api/sms/")
|
||||
group.POST("code", h.SendCode)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
|
||||
group := s.Engine.Group("/api/captcha/")
|
||||
group.GET("get", h.Get)
|
||||
group.POST("check", h.Check)
|
||||
group.GET("slide/get", h.SlideGet)
|
||||
group.POST("slide/check", h.SlideCheck)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PPTTaskHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
|
||||
group := s.Engine.Group("/api/redeem/")
|
||||
group.POST("verify", h.Verify)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
|
||||
group := s.Engine.Group("/api/mj/")
|
||||
group.POST("image", h.Image)
|
||||
group.POST("upscale", h.Upscale)
|
||||
group.POST("variation", h.Variation)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
|
||||
group := s.Engine.Group("/api/sd")
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
|
||||
group := s.Engine.Group("/api/config/")
|
||||
group.GET("get", h.Get)
|
||||
group.GET("license", h.License)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 管理后台控制器
|
||||
// 管理后台路由注册
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
|
||||
group := s.Engine.Group("/api/admin/config")
|
||||
group.POST("update", h.Update)
|
||||
group.GET("get", h.Get)
|
||||
group.POST("active", h.Active)
|
||||
group.GET("fixData", h.FixData)
|
||||
group.GET("license", h.GetLicense)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
|
||||
group := s.Engine.Group("/api/admin/")
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
group.GET("session", h.Session)
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("enable", h.Enable)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
|
||||
group := s.Engine.Group("/api/admin/apikey/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
|
||||
group := s.Engine.Group("/api/admin/user/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("loginLog", h.LoginLog)
|
||||
group.GET("genLoginLink", h.GenLoginLink)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) {
|
||||
group := s.Engine.Group("/api/admin/role/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("sort", h.Sort)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) {
|
||||
group := s.Engine.Group("/api/admin/redeem/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("create", h.Create)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("export", h.Export)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
|
||||
group := s.Engine.Group("/api/admin/dashboard/")
|
||||
group.GET("stats", h.Stats)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
|
||||
group := s.Engine.Group("/api/model/")
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
|
||||
group := s.Engine.Group("/api/admin/model/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("set", h.Set)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||
group := s.Engine.Group("/api/payment/")
|
||||
group.POST("doPay", h.Pay)
|
||||
group.GET("payWays", h.GetPayWays)
|
||||
group.POST("notify/alipay", h.AlipayNotify)
|
||||
group.GET("notify/geek", h.GeekPayNotify)
|
||||
group.POST("notify/wechat", h.WechatPayNotify)
|
||||
group.POST("notify/hupi", h.HuPiPayNotify)
|
||||
h.RegisterRoutes()
|
||||
h.StartSyncOrders()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
|
||||
group := s.Engine.Group("/api/admin/product/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
|
||||
group := s.Engine.Group("/api/admin/order/")
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("clear", h.Clear)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
|
||||
group := s.Engine.Group("/api/order/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("query", h.Query)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
|
||||
group := s.Engine.Group("/api/product/")
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewInviteHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
|
||||
group := s.Engine.Group("/api/invite/")
|
||||
group.GET("code", h.Code)
|
||||
group.GET("list", h.List)
|
||||
group.GET("hits", h.Hits)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(admin.NewFunctionHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
|
||||
group := s.Engine.Group("/api/admin/function/")
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("token", h.GenToken)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(admin.NewUploadHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
|
||||
s.Engine.POST("/api/admin/upload", h.Upload)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewFunctionHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
|
||||
group := s.Engine.Group("/api/function/")
|
||||
group.POST("weibo", h.WeiBo)
|
||||
group.POST("zaobao", h.ZaoBao)
|
||||
group.POST("dalle3", h.Dall3)
|
||||
group.POST("websearch", h.WebSearch)
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewChatHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
|
||||
group := s.Engine.Group("/api/admin/chat/")
|
||||
group.POST("list", h.List)
|
||||
group.POST("message", h.Messages)
|
||||
group.GET("history", h.History)
|
||||
group.GET("remove", h.RemoveChat)
|
||||
group.GET("message/remove", h.RemoveMessage)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) {
|
||||
group := s.Engine.Group("/api/powerLog/")
|
||||
group.POST("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.PowerLogHandler) {
|
||||
group := s.Engine.Group("/api/admin/powerLog/")
|
||||
group.POST("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewMenuHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) {
|
||||
group := s.Engine.Group("/api/admin/menu/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewMenuHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) {
|
||||
group := s.Engine.Group("/api/menu/")
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewMarkMapHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
|
||||
s.Engine.POST("/api/markMap/gen", h.Generate)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewDallJobHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
|
||||
group := s.Engine.Group("/api/dall")
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.GET("models", h.GetModels)
|
||||
fx.Provide(handler.NewImageJobHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ImageJobHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewSunoHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
|
||||
group := s.Engine.Group("/api/suno")
|
||||
group.POST("create", h.Create)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("detail", h.Detail)
|
||||
group.GET("play", h.Play)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewVideoHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
|
||||
group := s.Engine.Group("/api/video")
|
||||
group.POST("luma/create", h.LumaCreate)
|
||||
group.POST("keling/create", h.KeLingCreate)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 即梦AI 路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.JimengHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.VideoHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.SunoHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.PPTHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewChatAppTypeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
|
||||
group := s.Engine.Group("/api/admin/app/type")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewChatAppTypeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppTypeHandler) {
|
||||
group := s.Engine.Group("/api/app/type")
|
||||
group.GET("list", h.List)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewTestHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
|
||||
group := s.Engine.Group("/api/test")
|
||||
group.Any("sse", h.PostTest, h.SseTest)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewPromptHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
|
||||
group := s.Engine.Group("/api/prompt")
|
||||
group.POST("/lyric", h.Lyric)
|
||||
group.POST("/image", h.Image)
|
||||
group.POST("/video", h.Video)
|
||||
group.POST("/meta", h.MetaPrompt)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 微信公众号路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.WxGzhHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||
go func() {
|
||||
@@ -546,29 +443,17 @@ func main() {
|
||||
}),
|
||||
fx.Provide(admin.NewImageHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
|
||||
group := s.Engine.Group("/api/admin/image")
|
||||
group.POST("/list/mj", h.MjList)
|
||||
group.POST("/list/sd", h.SdList)
|
||||
group.POST("/list/dall", h.DallList)
|
||||
group.GET("/remove", h.Remove)
|
||||
}),
|
||||
fx.Provide(admin.NewMediaHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) {
|
||||
group := s.Engine.Group("/api/admin/media")
|
||||
group.POST("/suno", h.SunoList)
|
||||
group.POST("/videos", h.Videos)
|
||||
group.GET("/remove", h.Remove)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewRealtimeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) {
|
||||
s.Engine.Any("/api/realtime", h.Connection)
|
||||
s.Engine.POST("/api/realtime/voice", h.VoiceChat)
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
)
|
||||
// 启动应用程序
|
||||
go func() {
|
||||
if err := app.Start(context.Background()); err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -581,7 +466,7 @@ func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := app.Stop(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDszCCApugAwIBAgIQICMRB0rBU2/rZJbfJGMYIzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
|
||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDYzNTQxWhcNMjQxMTA2MDYzNTQxWjCB
|
||||
hDELMAkGA1UEBhMCQ04xHzAdBgNVBAoMFm1ib25meTkwMTVAc2FuZGJveC5jb20xDzANBgNVBAsM
|
||||
BkFsaXBheTFDMEEGA1UEAww65pSv5LuY5a6dKOS4reWbvSnnvZHnu5zmioDmnK/mnInpmZDlhazl
|
||||
j7gtMjA4ODcyMTAyMDc1MDU4MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsoKcw5
|
||||
sxaiyV7mpWzDtnQ1K518eQLP0+dJlZAf06aBep/Aj9DIqrba/k7DHt8dKQvILMLAMpN1+2IRxbaO
|
||||
yxMa/laj3lZ1eHrB6F077O3D62oHcE3noZtXL0N1zZAxpmkNmYIHeLZS2oLMS4ANu47O/wpDC7BV
|
||||
HjdpZugtdPJ4mxdCpM9GDdLs7W4s5QI4PUPK4skFNMFoKI+0cYP/9ju87UP//IHC/K510GWNl+Gn
|
||||
Cvgag3AmiIB0utJNsGhxm6zT1T9tUWjW9iz/BxBKiPatsCX9VpPQzGnW7ZonRQtiZSokIlP2IPvl
|
||||
H5DcwpWUz3/LUY0SmKxnKOEYeOOqCW8CAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQAtgxF2EzjOndEFxBUD9tFwcSt6XKGggOp52oft1pvynPg4ALTLafOtfEPDrFBH
|
||||
PwpYrSu9s9C8NJtaA2HrlCfBjIuwEFTXiN+HPvS0SwSPKt9AXEiTcOF8vDcGamEen8QI4fo5Jia7
|
||||
2VRKkerkww5/+FzSaVO7ZUKuL80M1QJStmAZc8kPPwdYOTTW2bGf8BcmSDL6SPElBkt7tCCRd4sn
|
||||
+jq4cZ0yb2i77rBZCwHcTvfTqIBblPwLv4uGvg3+83BxIB5w6Kqp06bKEAPmobFY5IVHa+ON0/qi
|
||||
BXxXr+WQ3piKRVQEN64+PTAjSc67Ix1umvpLl3Ko6Ry7NJmpDcUn
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDszCCApugAwIBAgIQIBkIGbgVxq210KxLJ+YA/TANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UE
|
||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxJTAjBgNVBAsMHENlcnRpZmljYXRpb24gQXV0
|
||||
aG9yaXR5IHRlc3QxNjA0BgNVBAMMLUFudCBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
|
||||
dHkgUjEgdGVzdDAeFw0xOTA4MTkxMTE2MDBaFw0yNDA4MDExMTE2MDBaMIGRMQswCQYDVQQGEwJD
|
||||
TjEbMBkGA1UECgwSQW50IEZpbmFuY2lhbCB0ZXN0MSUwIwYDVQQLDBxDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSB0ZXN0MT4wPAYDVQQDDDVBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
|
||||
aXR5IENsYXNzIDIgUjEgdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMh4FKYO
|
||||
ZyRQHD6eFbPKZeSAnrfjfU7xmS9Yoozuu+iuqZlb6Z0SPLUqqTZAFZejOcmr07ln/pwZxluqplxC
|
||||
5+B48End4nclDMlT5HPrDr3W0frs6Xsa2ZNcyil/iKNB5MbGll8LRAxntsKvZZj6vUTMb705gYgm
|
||||
VUMILwi/ZxKTQqBtkT/kQQ5y6nOZsj7XI5rYdz6qqOROrpvS/d7iypdHOMIM9Iz9DlL1mrCykbBi
|
||||
t25y+gTeXmuisHUwqaRpwtCGK4BayCqxRGbNipe6W73EK9lBrrzNtTr9NaysesT/v+l25JHCL9tG
|
||||
wpNr1oWFzk4IHVOg0ORiQ6SUgxZUTYcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQBWThEoIaQoBX2YeRY/I8gu6TYnFXtyuCljANnXnM38ft+ikhE5mMNgKmJYLHvT
|
||||
yWWWgwHoSAWEuml7EGbE/2AK2h3k0MdfiWLzdmpPCRG/RJHk6UB1pMHPilI+c0MVu16OPpKbg5Vf
|
||||
LTv7dsAB40AzKsvyYw88/Ezi1osTXo6QQwda7uefvudirtb8FcQM9R66cJxl3kt1FXbpYwheIm/p
|
||||
j1mq64swCoIYu4NrsUYtn6CV542DTQMI5QdXkn+PzUUly8F6kDp+KpMNd0avfWNL5+O++z+F5Szy
|
||||
1CPta1D7EQ/eYmMP+mOQ35oifWIoFCpN6qQVBS/Hob1J/UUyg7BW
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,88 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG
|
||||
EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw
|
||||
MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO
|
||||
UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE
|
||||
MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT
|
||||
V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti
|
||||
W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ
|
||||
MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b
|
||||
53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI
|
||||
pDoiVhsLwg==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIF0zCCA7ugAwIBAgIIH8+hjWpIDREwDQYJKoZIhvcNAQELBQAwejELMAkGA1UE
|
||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmlj
|
||||
YXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmlj
|
||||
YXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMyMTEzNDg0MFoXDTM4MDIyODEzNDg0
|
||||
MFowejELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNV
|
||||
BAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5j
|
||||
aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMIICIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAg8AMIICCgKCAgEAtytTRcBNuur5h8xuxnlKJetT65cHGemGi8oD+beHFPTk
|
||||
rUTlFt9Xn7fAVGo6QSsPb9uGLpUFGEdGmbsQ2q9cV4P89qkH04VzIPwT7AywJdt2
|
||||
xAvMs+MgHFJzOYfL1QkdOOVO7NwKxH8IvlQgFabWomWk2Ei9WfUyxFjVO1LVh0Bp
|
||||
dRBeWLMkdudx0tl3+21t1apnReFNQ5nfX29xeSxIhesaMHDZFViO/DXDNW2BcTs6
|
||||
vSWKyJ4YIIIzStumD8K1xMsoaZBMDxg4itjWFaKRgNuPiIn4kjDY3kC66Sl/6yTl
|
||||
YUz8AybbEsICZzssdZh7jcNb1VRfk79lgAprm/Ktl+mgrU1gaMGP1OE25JCbqli1
|
||||
Pbw/BpPynyP9+XulE+2mxFwTYhKAwpDIDKuYsFUXuo8t261pCovI1CXFzAQM2w7H
|
||||
DtA2nOXSW6q0jGDJ5+WauH+K8ZSvA6x4sFo4u0KNCx0ROTBpLif6GTngqo3sj+98
|
||||
SZiMNLFMQoQkjkdN5Q5g9N6CFZPVZ6QpO0JcIc7S1le/g9z5iBKnifrKxy0TQjtG
|
||||
PsDwc8ubPnRm/F82RReCoyNyx63indpgFfhN7+KxUIQ9cOwwTvemmor0A+ZQamRe
|
||||
9LMuiEfEaWUDK+6O0Gl8lO571uI5onYdN1VIgOmwFbe+D8TcuzVjIZ/zvHrAGUcC
|
||||
AwEAAaNdMFswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFF90
|
||||
tATATwda6uWx2yKjh0GynOEBMB8GA1UdIwQYMBaAFF90tATATwda6uWx2yKjh0Gy
|
||||
nOEBMA0GCSqGSIb3DQEBCwUAA4ICAQCVYaOtqOLIpsrEikE5lb+UARNSFJg6tpkf
|
||||
tJ2U8QF/DejemEHx5IClQu6ajxjtu0Aie4/3UnIXop8nH/Q57l+Wyt9T7N2WPiNq
|
||||
JSlYKYbJpPF8LXbuKYG3BTFTdOVFIeRe2NUyYh/xs6bXGr4WKTXb3qBmzR02FSy3
|
||||
IODQw5Q6zpXj8prYqFHYsOvGCEc1CwJaSaYwRhTkFedJUxiyhyB5GQwoFfExCVHW
|
||||
05ZFCAVYFldCJvUzfzrWubN6wX0DD2dwultgmldOn/W/n8at52mpPNvIdbZb2F41
|
||||
T0YZeoWnCJrYXjq/32oc1cmifIHqySnyMnavi75DxPCdZsCOpSAT4j4lAQRGsfgI
|
||||
kkLPGQieMfNNkMCKh7qjwdXAVtdqhf0RVtFILH3OyEodlk1HYXqX5iE5wlaKzDop
|
||||
PKwf2Q3BErq1xChYGGVS+dEvyXc/2nIBlt7uLWKp4XFjqekKbaGaLJdjYP5b2s7N
|
||||
1dM0MXQ/f8XoXKBkJNzEiM3hfsU6DOREgMc1DIsFKxfuMwX3EkVQM1If8ghb6x5Y
|
||||
jXayv+NLbidOSzk4vl5QwngO/JYFMkoc6i9LNwEaEtR9PhnrdubxmrtM+RjfBm02
|
||||
77q3dSWFESFQ4QxYWew4pHE0DpWbWy/iMIKQ6UZ5RLvB8GEcgt8ON7BBJeMc+Dyi
|
||||
kT9qhqn+lw==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICiDCCAgygAwIBAgIIQX76UsB/30owDAYIKoZIzj0EAwMFADB6MQswCQYDVQQG
|
||||
EwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UECwwXQ2VydGlmaWNh
|
||||
dGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNpYWwgQ2VydGlmaWNh
|
||||
dGlvbiBBdXRob3JpdHkgRTEwHhcNMTkwNDI4MTYyMDQ0WhcNNDkwNDIwMTYyMDQ0
|
||||
WjB6MQswCQYDVQQGEwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UE
|
||||
CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNp
|
||||
YWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRTEwdjAQBgcqhkjOPQIBBgUrgQQA
|
||||
IgNiAASCCRa94QI0vR5Up9Yr9HEupz6hSoyjySYqo7v837KnmjveUIUNiuC9pWAU
|
||||
WP3jwLX3HkzeiNdeg22a0IZPoSUCpasufiLAnfXh6NInLiWBrjLJXDSGaY7vaokt
|
||||
rpZvAdmjXTBbMAsGA1UdDwQEAwIBBjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRZ
|
||||
4ZTgDpksHL2qcpkFkxD2zVd16TAfBgNVHSMEGDAWgBRZ4ZTgDpksHL2qcpkFkxD2
|
||||
zVd16TAMBggqhkjOPQQDAwUAA2gAMGUCMQD4IoqT2hTUn0jt7oXLdMJ8q4vLp6sg
|
||||
wHfPiOr9gxreb+e6Oidwd2LDnC4OUqCWiF8CMAzwKs4SnDJYcMLf2vpkbuVE4dTH
|
||||
Rglz+HGcTLWsFs4KxLsq7MuU+vJTBUeDJeDjdA==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDxTCCAq2gAwIBAgIUEMdk6dVgOEIS2cCP0Q43P90Ps5YwDQYJKoZIhvcNAQEF
|
||||
BQAwajELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM
|
||||
E0NoaW5hIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMMH2lUcnVzQ2hpbmEgQ2xhc3Mg
|
||||
MiBSb290IENBIC0gRzMwHhcNMTMwNDE4MDkzNjU2WhcNMzMwNDE4MDkzNjU2WjBq
|
||||
MQswCQYDVQQGEwJDTjETMBEGA1UECgwKaVRydXNDaGluYTEcMBoGA1UECwwTQ2hp
|
||||
bmEgVHJ1c3QgTmV0d29yazEoMCYGA1UEAwwfaVRydXNDaGluYSBDbGFzcyAyIFJv
|
||||
b3QgQ0EgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPPShpV
|
||||
nJbMqqCw6Bz1kehnoPst9pkr0V9idOwU2oyS47/HjJXk9Rd5a9xfwkPO88trUpz5
|
||||
4GmmwspDXjVFu9L0eFaRuH3KMha1Ak01citbF7cQLJlS7XI+tpkTGHEY5pt3EsQg
|
||||
wykfZl/A1jrnSkspMS997r2Gim54cwz+mTMgDRhZsKK/lbOeBPpWtcFizjXYCqhw
|
||||
WktvQfZBYi6o4sHCshnOswi4yV1p+LuFcQ2ciYdWvULh1eZhLxHbGXyznYHi0dGN
|
||||
z+I9H8aXxqAQfHVhbdHNzi77hCxFjOy+hHrGsyzjrd2swVQ2iUWP8BfEQqGLqM1g
|
||||
KgWKYfcTGdbPB1MCAwEAAaNjMGEwHQYDVR0OBBYEFG/oAMxTVe7y0+408CTAK8hA
|
||||
uTyRMB8GA1UdIwQYMBaAFG/oAMxTVe7y0+408CTAK8hAuTyRMA8GA1UdEwEB/wQF
|
||||
MAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBLnUTfW7hp
|
||||
emMbuUGCk7RBswzOT83bDM6824EkUnf+X0iKS95SUNGeeSWK2o/3ALJo5hi7GZr3
|
||||
U8eLaWAcYizfO99UXMRBPw5PRR+gXGEronGUugLpxsjuynoLQu8GQAeysSXKbN1I
|
||||
UugDo9u8igJORYA+5ms0s5sCUySqbQ2R5z/GoceyI9LdxIVa1RjVX8pYOj8JFwtn
|
||||
DJN3ftSFvNMYwRuILKuqUYSHc2GPYiHVflDh5nDymCMOQFcFG3WsEuB+EYQPFgIU
|
||||
1DHmdZcz7Llx8UOZXX2JupWCYzK1XhJb+r4hK5ncf/w8qGtYlmyJpxk3hr1TfUJX
|
||||
Yf4Zr0fJsGuv
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,19 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDmTCCAoGgAwIBAgIQICMRB2LW76yahgdg3IFNPDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
|
||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDU0NjE5WhcNMjQxMTExMDU0NjE5WjBr
|
||||
MQswCQYDVQQGEwJDTjEfMB0GA1UECgwWbWJvbmZ5OTAxNUBzYW5kYm94LmNvbTEPMA0GA1UECwwG
|
||||
QWxpcGF5MSowKAYDVQQDDCEyMDg4NzIxMDIwNzUwNTgxLTkwMjEwMDAxMzE2NTgwMjMwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxihQPf1Q+g9ArgM46shVqL5sbRha/df95D1PsWyEq
|
||||
ANmWmG4zZ+ksYDVQrc4KzhSRoi56sm/7TDFYTmM6bW99e/nKW58WxyZB4ie5qA3F4n17psPyDqb8
|
||||
IokcQmCphSFDaXQD6AoXoLNtTM0vAI2cWxAgebZ/vsrdj5Ntjt+Rp3NYMCk1i5xovHcfILzLEGbX
|
||||
QXoT9fo5AhHotTWa6xHVLPUGY9qwLzQxHzBmvy5ZMfnOfJkm/mDisTSqAUB59F3dzU/1ARVkEZ1w
|
||||
Mgb4XohWBw6iurQfbMnH2mIomAAwwZVFv+sXDbL9yMbSMo/SjVsTQprn0Q0EnwLo7nmmOM6HAgMB
|
||||
AAGjEjAQMA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEAn3Y4/C1h9R6ONsBqX3/q
|
||||
XfHX7yX1FM0Y1x48X3/Yxk6HivAkTukhhhVYVKJsbrbzRqHDp9vhAP/FR6o6pAevaYMmLov0VMXU
|
||||
7oAuetgkaYEYkDuNen5/Hpdhqi2vTtdT+q9w8zHJd6MDQ0aoHgIxpLKw5vof2R1N4fwSgNXMiXE5
|
||||
kmllKQMem/+on2p+Sj80/2asxryHIGlH87qPzkffv+kIOkZthbTApTFLLjdVri2QHGe8/cc4xy01
|
||||
/9iR3IUzNahotT41lJ4bMevBY7XMAS3n5ekyABN/9ZRJqhWdXgmFCRN/u56qd6lDgu7R2M2QUoyc
|
||||
LuW5DfgRItKlmUB7sw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1 +0,0 @@
|
||||
MIIEpQIBAAKCAQEAsYoUD39UPoPQK4DOOrIVai+bG0YWv3X/eQ9T7FshKgDZlphuM2fpLGA1UK3OCs4UkaIuerJv+0wxWE5jOm1vfXv5ylufFscmQeInuagNxeJ9e6bD8g6m/CKJHEJgqYUhQ2l0A+gKF6CzbUzNLwCNnFsQIHm2f77K3Y+TbY7fkadzWDApNYucaLx3HyC8yxBm10F6E/X6OQIR6LU1musR1Sz1BmPasC80MR8wZr8uWTH5znyZJv5g4rE0qgFAefRd3c1P9QEVZBGdcDIG+F6IVgcOorq0H2zJx9piKJgAMMGVRb/rFw2y/cjG0jKP0o1bE0Ka59ENBJ8C6O55pjjOhwIDAQABAoIBAFetNfz1R7hbxjlFshMAkVzQR8wvT9qbvl+dtzdZRcaFhu89NecDIP7+QDYor0FcxoGpU0TazDyRQyk2BQD8vHt+9zv9BVLtZLJSqoWgPbUFBi1DjS8EF2ka8RVYnn35NhUhhd7L//ftL88Bh673mfembQ9srDjoEy1Z01feoABAnCMkNFl986DmEwnarvEufXSDIgeN4ioMxha4NvfIPuI0zpVdV1O9sv+SGC+VEWZBtN3GNsaf4zS/f8FVGvTiU/Abz0gSw/iwSPHclDWQDTN3yFHf/tfqlzh0mH0WfhnuOBFWXzK+R7fbnM+asI9ttvzRcfpzgRGXdPcNcOv/6cECgYEA3DVqpi1k8MYfJixju6SG5gfyhM4VFksFmCMaNPgtatDMBKLMTgV/Ej6LXREojcy29uZl83F09pVlpd41eG39ULIPktixA/BqErQ2UaWh6kOxifycpu22Jh0r09hax6UgVrcBrrnCJEjcFsuJlrZvXQSzc3PBxjWy5gjabS5h9iECgYEAzmVAIh2frF01Y95zsLueAhhZwCtPanm6kf7ivR4r1plIX3b2sNRhWGmEHFgaCE6Braa0ogQ73Hd26kw4ZW+D6QMGC/zjCBEzDLLf++SjdVUHiY5AR4WHqXzq1jdAlsVyo9R661oAOp3lhiJVGLNXkHyEfEVPHsaxJh4osYSbX6cCgYEAx32Qx0i6eDFTyLZQB46uMrgiaVN04QRH5iJuvGvUYT8UhGKjaU8rZfDJOh+wOH2rhxMEaz1uc3C2bERY9mfWI4Ob/jFWc7YZsiYWS3Mcsuhubw4tMECLUg39RWZsHw8ls8kIuixIh6yFzhTH6YQOcRswIrhMZG8DScfdcSmiz2ECgYEAkWP1t5KSpkLKl11etcKUXfl1T8+yk9jIOowIgRw92WAFAWq2AH67TCKYM7dEL1HOO9tRJ0hAOt/U3ttuZtYVYBEHM26jJ02mXm2rJrA7DS4mrxmL4lYH6LbcXqZxU0Qnq4zEQgIWYzRTORf6Rfof1uJAGaJhR9bDd4yLMfGt2cUCgYEAo216Y61xOHUTA4AF1eekk+r+uOcQgQDvLXfs9FkDdJLk0mPG48/+eIYpPFnANJ/riF/DWOp8WGEe2IzA9yUFexzDbNQK8ha9kGcxaSAyiCwzjZ/t9/+hScDSV8kNqWSRSisu/YOFleEHbokT6mbLZ+gdqES8mUUanaEBzRQYGxo=
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user