mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-08-22 07:17:13 +00:00
Compare commits
2314 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 | |||
| 598f6c48fb | |||
| 97e489901a | |||
| eea57790de | |||
| 81b32523ed | |||
| 6b6fe1bebd | |||
| a7063bf30a | |||
| 32fc4d86a2 | |||
| e685876cc0 | |||
| 41e4b1c7ac | |||
| 76a3ada85f | |||
| b1ddcef593 | |||
| 94a5187e75 | |||
| 521ca77541 | |||
| 5dd3c1835a | |||
| f08329eef4 | |||
| e993a582cd | |||
| 1524e189dc | |||
| 9fb01ee3ee | |||
| 9d72edc048 | |||
| a9505ff72d | |||
| 628ae15fd7 | |||
| 1ce71374ac | |||
| 7c81d946a7 | |||
| cdaeb2a404 | |||
| ca2de54438 | |||
| 303e9ed052 | |||
| 347b640614 | |||
| 19099aed6f | |||
| 615515094b | |||
| c4fe6c825e | |||
| 26c18fcd5a | |||
| a914994483 | |||
| 9edd6621b1 | |||
| 10e3e61b2c | |||
| 643cf6085a | |||
| fa74ae18ee | |||
| dffdbf697b | |||
| 5e59b3a708 | |||
| 7bc55f3ed1 | |||
| 73f5a44e0a | |||
| 2c6abbe7e4 | |||
| 1f0cf11636 | |||
| c44f5d40fe | |||
| 314d81303b | |||
| b9859e5591 | |||
| a3d65ba939 | |||
| 8a2d2f66b5 | |||
| d1c9fd6eba | |||
| d629d842be | |||
| f752ec5b06 | |||
| c7b09f29ca | |||
| 51c270fb29 | |||
| b97d4b7895 | |||
| 0627109b2b | |||
| c2d4530395 | |||
| c3be47d4ce | |||
| a1acca6f7a | |||
| 2b5165324c | |||
| 8f67826072 | |||
| ac9a31f049 | |||
| ccfc9f17e9 | |||
| 4641482865 | |||
| 79522d9ab5 | |||
| e0b4e8970a | |||
| 4d93e901e0 | |||
| bcc72a3091 | |||
| 1c1ddf76fb | |||
| ed2cdbbc31 | |||
| 04caf92702 | |||
| c797b35f5a | |||
| 0746cd49f4 | |||
| a3a2500498 | |||
| ff69cb231a | |||
| afb9193985 | |||
| 14fa4fdaa0 | |||
| 2a71d5d557 | |||
| cd31333d0c | |||
| f080425ee6 | |||
| 8bd2feaf7e | |||
| 4eccfe6d2c | |||
| 5ba2e000a1 | |||
| fe9adadb3a | |||
| 805c2a045e | |||
| fd7b196134 | |||
| 1b1c327c35 | |||
| a228f603d2 | |||
| 96dd0ddb99 | |||
| a4ee5cdeff | |||
| d0025032b0 | |||
| 7d0a05ee11 | |||
| 5f0a62b63e | |||
| 825a1b1027 | |||
| 38dde1a373 | |||
| 950e7d1b00 | |||
| 89ded74345 | |||
| d8f9f48278 | |||
| 818bb38617 | |||
| 43f00b1481 | |||
| f580f671a3 | |||
| b1fb16995a | |||
| 47907b9f0c | |||
| ba55fca7cc | |||
| f687a10416 | |||
| 393bfa137e | |||
| 4dcb0d850c | |||
| 88fa374104 | |||
| e1b1c195f6 | |||
| 1352369af0 | |||
| ded041da0f | |||
| 7b9a7475a9 | |||
| 3958e99e4d | |||
| 0ef51714c9 | |||
| 668ff70bc1 | |||
| ed063a1d9d | |||
| 88eaddbd1d | |||
| 8369e18bf0 | |||
| 79b9476d3d | |||
| 41bfa3a974 | |||
| 6b0d4e81bf | |||
| 41e66d85d5 | |||
| f98dcee7d4 | |||
| 04b364c1cd | |||
| 6c84d2557c | |||
| 8a4596b36a | |||
| 6e2deeed87 | |||
| bf6834da4e | |||
| 68dcd054f9 | |||
| bdd76addf3 | |||
| 0202d75ff4 | |||
| a77bebbc29 | |||
| 2936f21f12 | |||
| a0831ec6cd | |||
| 705ca4d20a | |||
| 2926aab7a4 | |||
| 36beb74de8 | |||
| dd1f98db1e | |||
| b7f41c524a | |||
| a3f0576535 | |||
| 5c8a237e27 | |||
| 447adf45eb | |||
| ca77288a69 | |||
| 63be3f5f56 | |||
| cad1ce6943 | |||
| 54b5a78c0e | |||
| 98d4d58393 | |||
| 887fdb6679 | |||
| 63fd125439 | |||
| c39dd913fd | |||
| b40f7ed5f3 | |||
| 183829a08b | |||
| 03d33c784c | |||
| eec10fdfbc | |||
| 0a3c74cd6f | |||
| 5768c7959e | |||
| d124eddd9d | |||
| 6a066f1b8e | |||
| 775fb1f853 | |||
| b7d137247a | |||
| 22278c40bf | |||
| 4373642ebd | |||
| e29b32bd2d | |||
| 5bf90920f5 | |||
| fa6bb5e46a | |||
| edcbb3e226 | |||
| cebf8497a4 | |||
| dd675c9a9b | |||
| f975f9b0b8 | |||
| fbefe5b308 | |||
| 312abbc273 | |||
| 8ced447a14 | |||
| 5213bdf08b | |||
| cded4bdcc7 | |||
| cf817fd8ea | |||
| 596b700d63 | |||
| f8e32148c8 | |||
| 2c899f6057 | |||
| be799000ee | |||
| 22cb2270af | |||
| 4e440b7910 | |||
| 4e6f14cb9e | |||
| 8dc03a7509 | |||
| 57b1b44645 | |||
| aa17a33093 | |||
| 80e27c40e9 | |||
| 8250e876a5 | |||
| 9f98491368 | |||
| a2481ff1cf | |||
| 59dfa95bf8 | |||
| fe160f978b | |||
| 7da5b7163c | |||
| cffc722622 | |||
| a7baf1dc9e | |||
| 488169683f | |||
| 2ba3c52e6e | |||
| 18179613fc | |||
| 8af0fec8ec | |||
| acee2d9d81 | |||
| cbf06eea24 | |||
| 989b4a64d6 | |||
| b01b10014a | |||
| e857f98e5c | |||
| 274cff71b1 | |||
| 06573c5d12 | |||
| 937e5befa2 | |||
| fb403bde8b | |||
| ba174ef3ee | |||
| b7b702862f | |||
| 6df2b5735b | |||
| 130e151a06 | |||
| ab903e3cc1 | |||
| bc7d06d3e5 | |||
| f6b5a94b29 | |||
| 237387b2ab | |||
| 0c1f650e9c | |||
| 357c77ef30 | |||
| dc7c049a7b | |||
| 8e81dfa12a | |||
| 188fb23f08 | |||
| 0ff76f0f21 | |||
| 5347a12035 | |||
| 787caa84c8 | |||
| 691d453f41 | |||
| c2503e663a | |||
| 5b7f2603ae | |||
| 710b008453 | |||
| 6e7aecc568 | |||
| 405a88862b | |||
| 3fd7f810f0 | |||
| 296eabe09a | |||
| e04a48623d | |||
| b68f7e3fd1 | |||
| d30d5585c6 | |||
| 2e1bad387c | |||
| 1b7c7a0dc1 | |||
| 207f2b5ac4 | |||
| d13fa1392f | |||
| 9bf886fe98 | |||
| aeef77ac24 | |||
| 9a97a1ee72 | |||
| 54b45ec2ff | |||
| 5c18a50330 | |||
| c434f85045 | |||
| d78d3ffe02 | |||
| 4d10279870 | |||
| 44240f65d5 | |||
| 6aaf607ed7 | |||
| 97e81a7dcc | |||
| cff0397735 | |||
| 9e8f1ed6bf | |||
| 2aa0b51c09 | |||
| 4dbfdab50d | |||
| ce8a2d0222 | |||
| c39814ce2b | |||
| 135755d21d | |||
| 95a071014c | |||
| 5be4e83876 | |||
| 6e03f4b363 | |||
| 9de9489673 | |||
| bb8644dea0 | |||
| cbc9eb3a59 | |||
| ff9142ddd4 | |||
| 0593359ef8 | |||
| 9814fec930 | |||
| 35f469fb82 | |||
| 53ba731159 | |||
| 705ad58a8f | |||
| 2081d3ce29 | |||
| 41d9c097e8 | |||
| c96f86fbbf | |||
| 1fe1e40a43 | |||
| 4b3b64e9e2 | |||
| ad6e2dd370 | |||
| 2f2e146951 | |||
| bb63f23414 | |||
| a09c529414 | |||
| 43f6bf74f2 | |||
| a8d1d58e95 | |||
| 662d7b099e | |||
| 6695be815e | |||
| d5eeeea764 | |||
| fbd3478772 | |||
| 43c507c597 | |||
| 2102e1afbb | |||
| e356771049 | |||
| 155c56f502 | |||
| 48139290ed | |||
| 37a9b0e485 | |||
| bd852c82b7 | |||
| 52a3f13f1e | |||
| 13564993d7 | |||
| a678a11c33 | |||
| bfc1e1bc2c | |||
| d34b785238 | |||
| ba20717a09 | |||
| 52e40daf23 | |||
| 1086e1d631 | |||
| b2f57aa483 | |||
| 3094b9c8fd | |||
| 4c2dba1004 | |||
| 0f0a4c0a7e | |||
| 430a7b2297 | |||
| c374126f69 | |||
| c91a38a882 | |||
| 8498cd71dc | |||
| 6e02bee4b7 | |||
| bf30517393 | |||
| b62218110e | |||
| 495f86e7fc | |||
| e2960b2607 | |||
| 471017657f | |||
| 88e7c39066 | |||
| 4861dd75be | |||
| 2a6dd636fa | |||
| 342ceea371 | |||
| 6bf38f78d5 | |||
| 880c34dfee | |||
| 5a04a935be | |||
| a1e487100d | |||
| 8923e938d2 | |||
| 77948b1e16 | |||
| 1a1734abf0 | |||
| e28a12a1ee | |||
| 8093a3eeb2 | |||
| 00a8bc6784 | |||
| 9edb3d0a82 | |||
| 8fffa60569 | |||
| d95fab11be | |||
| 2debe7e927 | |||
| 6ef09c8ad5 | |||
| 478bc32ddd | |||
| 283a023a06 | |||
| dfd2be1265 | |||
| d315edef5f | |||
| 33ff2e29e5 | |||
| 5fa17b300e | |||
| 4df1c7a136 | |||
| 32919de7a7 | |||
| c522248d39 | |||
| 7d126aab41 | |||
| 246d96ee63 | |||
| 16ac57ced3 | |||
| 909ae4aa00 | |||
| 4976b967e7 | |||
| 26e3ababcf | |||
| e874178782 | |||
| 4d9f89f630 | |||
| 8cb66ad01b | |||
| b989199edb | |||
| f887a39912 | |||
| 0d81fe6c8e | |||
| 2beffd3dd3 | |||
| be45f41e34 | |||
| d8cb92d8d4 | |||
| e9ac58b1ef | |||
| 158db83965 | |||
| 59d9ae96ac | |||
| 603bfa7def | |||
| 3cff6f7189 | |||
| 829fb879a6 | |||
| 14aee28289 | |||
| 0385e60ce1 | |||
| 8c1b4d4516 | |||
| aaea23f785 | |||
| 068df8fa15 | |||
| 131efd6ba5 | |||
| e371310d02 | |||
| 866564370d | |||
| 96b8121210 | |||
| dcdc0d8918 | |||
| 1960a85ead | |||
| 6c7fa17e50 | |||
| 69cb479e01 | |||
| 38a0d00142 | |||
| db90131bd6 | |||
| 5c77e67b0f | |||
| c18d272413 | |||
| 961cee5e41 | |||
| cd0952e170 | |||
| c9cc93be8c | |||
| e6a6d0485d | |||
| 49f2e1a71e | |||
| a642ae0332 | |||
| 97eff6085a | |||
| 67f508c7da | |||
| 8b2e2d61af | |||
| 2225f09797 | |||
| c096efb416 | |||
| fe7fa46a0c | |||
| cdaf6fb9dc | |||
| 6d8901fa21 | |||
| 78f443ed6d | |||
| 7750cb0021 | |||
| 54e8d72b10 | |||
| 327d68a347 | |||
| 05161f48fd | |||
| 7c272b0efd | |||
| e971bf6b88 | |||
| 5eb4ed157e | |||
| 55b979784c | |||
| d4e00b960c | |||
| 79adc871ef | |||
| af0eff88b8 | |||
| 97aa922b5f | |||
| c755befa28 | |||
| 11c760a4e8 | |||
| 3f6890da27 | |||
| 8144fada25 | |||
| 08319391ea | |||
| 87b03332d9 | |||
| ae7f1c03e2 | |||
| 8b14eeadf4 | |||
| 151017ed47 | |||
| e0ead127e0 | |||
| 941d15b635 | |||
| 0887bcdee0 | |||
| d8058400dd | |||
| 67d83041d7 | |||
| 5a1ed953de | |||
| 1350f388f0 | |||
| 58d127b602 | |||
| 65dde9e69d | |||
| 13bf73e6cf | |||
| 2e5bd238b7 | |||
| aa46fdb113 | |||
| 8fc8fd6cba | |||
| 4a09da3a25 | |||
| dfc6c87250 | |||
| d70fcbb9f3 | |||
| b63e01225e | |||
| a16ef6476d | |||
| 561b82027a | |||
| dae75343d0 | |||
| f6d8fbf570 | |||
| d7564127d2 | |||
| 568201ebbb | |||
| c6bf54fd9d | |||
| ab421f2185 | |||
| 8d47072e1c | |||
| f71a2f5263 | |||
| d601226187 | |||
| d000cc5a67 | |||
| 4801832d9d | |||
| 04d6ba0853 | |||
| 6b0f42d0b8 | |||
| 8d7c028ca8 | |||
| 08379c21ac | |||
| 3ae7ebfeaf | |||
| 2499bd9ad3 | |||
| aa42d38387 | |||
| ad6da0d320 | |||
| 43843b92f2 | |||
| a2583c0591 | |||
| 5da879600a | |||
| 0ea4a29152 | |||
| 87ed2064e3 | |||
| 7fe50788a5 | |||
| 34e96e91d4 | |||
| c4a68076d6 | |||
| 8c4c2b89ce | |||
| 9e365895af | |||
| 373021c191 | |||
| 9ece25e17d | |||
| 740c3c1b00 | |||
| 2140eff51b | |||
| 67c7132e6b | |||
| dfba3c30a5 | |||
| c77843424b | |||
| 4b717109d2 | |||
| 2d4959aa7d | |||
| e54e908fbc | |||
| 167c59a159 | |||
| 7a11a9ef15 | |||
| 1d0006ce59 | |||
| c9d0700fd9 | |||
| 6a8b4ee2f1 | |||
| f9b809801d | |||
| 72b1515b68 | |||
| cc551ba266 | |||
| 754ba02263 | |||
| 23787ff462 | |||
| 7ddf57ae06 | |||
| 9f603c5e77 | |||
| 3f0252b498 | |||
| 3d720bdd81 | |||
| cc5180a6f7 | |||
| a8eccbe43b | |||
| 1d9d487f0e | |||
| 2ac77ac39f | |||
| 96f1126d02 | |||
| 4eaa518cf3 | |||
| 7f9b8d8246 | |||
| 5622f94fc8 | |||
| 5132d52a44 | |||
| 95457b7dcd | |||
| 1bcbf74883 | |||
| 6a9de72c78 | |||
| abdf5298fe | |||
| 088a614160 | |||
| 2129f7a8b7 | |||
| 013ee98f53 | |||
| f6f8748521 | |||
| dc46ed0e05 | |||
| 59301df073 | |||
| 2a0c657ca3 | |||
| e17dcf4d5f | |||
| a0aee80c63 | |||
| 09f44e6d9b | |||
| 4910be3403 | |||
| 59824bffc5 | |||
| 1541d74c84 | |||
| cb0dacd5e0 | |||
| 34c9151dc1 | |||
| 7463cfc66c | |||
| 6c69770ed6 | |||
| b248560ba2 | |||
| 5b7c38c67f | |||
| 37368fe13f | |||
| b7bec8ecb7 | |||
| 246b023624 | |||
| 82fe9c3596 | |||
| 9f44c34d34 | |||
| afd84516b0 | |||
| a6b9f57a50 | |||
| 024f00b73c | |||
| 42bc23cacf | |||
| 24a21bf2ee | |||
| 282f55c7a3 | |||
| f22c6bf658 | |||
| 44798f89ba | |||
| 0df700ec18 | |||
| 596cb2b206 | |||
| 46141f87b8 | |||
| d1965deff1 | |||
| eecce10018 | |||
| b793b81768 | |||
| c8506e3d3b | |||
| a5ef4299ec | |||
| bddd611cc1 | |||
| cdb1a8bde1 | |||
| b399fc557a | |||
| 233f6e00f0 | |||
| b20ea734fd | |||
| b7dba68549 | |||
| 7ab0acd6a4 | |||
| 64e5fc48ba | |||
| 1d60ccc516 | |||
| a692cf1338 | |||
| 2a8d2213b1 | |||
| 6998dd7af4 | |||
| a27ce36a32 | |||
| 9343c73e0f | |||
| 3fdcc895ed | |||
| 739cd46539 | |||
| 88e510d07a | |||
| f8fed83507 | |||
| 2526feb0d9 | |||
| d63536d5ef | |||
| 4171b2bdfb | |||
| 4905fb28d4 | |||
| 0611f8de6c | |||
| a3a2a8abcb | |||
| df9e495dc5 | |||
| 839dd8dbf4 | |||
| 8a48476105 | |||
| 0375164f40 | |||
| 9d4a1705a5 | |||
| 691294b444 | |||
| 871e5733c7 | |||
| bdea12c51a | |||
| e8a663e3c7 | |||
| a27d9ea259 | |||
| 422d439627 | |||
| 7cd824c284 | |||
| edeff3c169 | |||
| e27d95e2b5 | |||
| 13076421a1 | |||
| c24b4d7074 | |||
| 3943e7f05f | |||
| 6839827db0 | |||
| cd0d9dad98 | |||
| ab24398748 | |||
| 857da34b9c | |||
| 6110522b54 | |||
| ccad7e7bb5 | |||
| bcdf5e3776 | |||
| b8de15d66e | |||
| 2207830db9 | |||
| c02661ea29 | |||
| d52dfbfef4 | |||
| 3c70c8ae59 | |||
| d6a04f96fe | |||
| b985b62068 | |||
| 66ccb387e8 | |||
| 297b760293 | |||
| 5f820b9dc1 | |||
| 088abfe7ab | |||
| 3cc2263dc7 | |||
| 17340ae0ac | |||
| f0a3c5d8ae | |||
| fe687af8ca | |||
| 2a4ef27774 | |||
| 7e26029268 | |||
| 2b057f32aa | |||
| 03f7f2a53e | |||
| bc6451026f | |||
| 296260bf6a | |||
| 99fd596862 | |||
| dd07acd21a | |||
| f0959b5df6 | |||
| 89f6402fbf | |||
| 6788edbe9d | |||
| 853059c814 | |||
| 3895305882 | |||
| 5a0876f8dc | |||
| 1b0938b33f | |||
| 2e32238652 | |||
| c2acbaaa94 | |||
| ad1a99aa44 | |||
| 02faff461a | |||
| 24e4be019a | |||
| e18e5a38c6 | |||
| 13c917ad7e | |||
| 2f9b1b7835 | |||
| 0a8cf6870f | |||
| 717b137a6d | |||
| 061291cebb | |||
| f755bdccae | |||
| b8e0d7760b | |||
| 4bba77ab47 | |||
| 962de0183c | |||
| 6944a32ff3 | |||
| 627396dbf7 | |||
| 5742b40aee | |||
| 6c300bb018 | |||
| 7f1ec90748 | |||
| 9273df4af2 | |||
| 4a99be2f15 | |||
| 669d784f54 | |||
| bee19392c1 | |||
| 5253d657b6 | |||
| 27c816cf3b | |||
| c2548c5007 | |||
| 0d81776212 | |||
| bdec823148 | |||
| 00d31a2379 | |||
| a6de958daa | |||
| cccab31c0f | |||
| 50c25d4574 | |||
| 5d65505ab7 | |||
| 68100f7f24 | |||
| 3dc7d0516a | |||
| 38777ea285 | |||
| 50335ebc2d | |||
| 00c8f08179 | |||
| bcadee7290 | |||
| 45d6579fb6 | |||
| cac3194d5b | |||
| dad9254128 | |||
| 4ddf3bf2bf | |||
| 9934143f00 | |||
| d45f9fbad6 | |||
| f617bde81b | |||
| d98b08d7cd | |||
| b2771b7b3f | |||
| 5a8fe5a6cf | |||
| 44b3237dd6 | |||
| 36c27d6092 | |||
| b987a2d365 | |||
| 3ab29da8f0 | |||
| 1cf299f090 | |||
| 3699f024f1 | |||
| c87d78c666 | |||
| 3d37a3d367 | |||
| 79c8d90049 | |||
| 73d8236697 | |||
| 1afc3e5e1c | |||
| 114d0088dc | |||
| 87e8bbfa41 | |||
| 43b6665370 | |||
| 3e3d3e162e | |||
| 5fb9f84182 | |||
| cc80b87fee | |||
| e35c34ad9a | |||
| b614ecccc2 | |||
| 1a4d798f8b | |||
| 8251c6589b | |||
| afb91a7023 | |||
| 4edf2ea02a | |||
| dc4c1f7877 | |||
| 12dd4659cd | |||
| bbc8fe2b40 | |||
| 5d4f40c004 | |||
| 3c34e8e0e7 | |||
| 7441a3a717 | |||
| 57c932f07c | |||
| 3eef9a836b | |||
| 922202734a | |||
| fffd7e375f | |||
| 8b3b0139b0 | |||
| d87f09c957 | |||
| 31828a3336 | |||
| b29884f8df | |||
| b270960a04 | |||
| c0ee2c2d8e | |||
| 5c4899df6e | |||
| be1580e949 | |||
| 9a797bb4a5 | |||
| 6424eb871c | |||
| b0c9ffc5a6 | |||
| 8a8c43c7a5 | |||
| f527cc5b98 | |||
| 9a503ddc72 | |||
| debe8dc209 | |||
| b130466c8f | |||
| 2f0215ac87 | |||
| 3ab5459778 | |||
| dd5cc206e5 | |||
| 4eb1bf2de2 | |||
| 142cd553a3 | |||
| 6c47551b03 | |||
| 657ecccee3 | |||
| b4ba70c0f3 | |||
| 1232c3cd9c | |||
| c2bf5e845a | |||
| 3ac04a3938 | |||
| 8900e72e45 | |||
| b7abc42209 | |||
| 0cb192135d | |||
| a48179ce0e | |||
| c1ca687630 | |||
| e589f25a05 | |||
| 29cc24508b | |||
| cc1a3ce343 | |||
| 3db55cbd48 | |||
| 7bb76d581c | |||
| c0a7f41747 | |||
| 0d733c0be0 | |||
| 1ba08bbfa6 | |||
| 8b40ac5b5c | |||
| 51a8f42d89 | |||
| 24479814e9 | |||
| 3b081ff0f4 | |||
| 99df028237 | |||
| 7f31a301e3 | |||
| b354b88876 | |||
| 039d949523 | |||
| 5e0be4d10e | |||
| 5b16dc6ee7 | |||
| 468b48151f | |||
| 34648c704f | |||
| fa5c036041 | |||
| 1966e69460 | |||
| 0fdc588167 | |||
| 74b1b07b31 | |||
| 2e023cb8dc | |||
| 118c0e1ff1 | |||
| e933f32d9c | |||
| 81d075c028 | |||
| bd4b0c4d65 | |||
| 514dd6c76a | |||
| 0b2501c1d8 | |||
| ddc323a8c3 | |||
| 9d28e62142 | |||
| 2933c057a2 | |||
| c1d892069e | |||
| 20ed6d1d3d | |||
| 61b2dbc9f1 | |||
| 90f5275201 | |||
| be3245666e | |||
| fe64b203da | |||
| dacdd6fe74 | |||
| 9eb1353455 | |||
| 6807f7e88a | |||
| 8a1c55c731 | |||
| 087f5ab2d1 | |||
| 9059bebd07 | |||
| 47c5a0387b | |||
| d18f79fe74 | |||
| f9da18ad52 | |||
| 3a8f0be28a | |||
| 5c9025ca22 | |||
| 60cf380f96 | |||
| d02cb573fd | |||
| ab8240613e | |||
| caa538a1d0 | |||
| d6e9dc6839 | |||
| b584b4bfb6 | |||
| 6891bdde53 | |||
| bda335212d | |||
| ecadfeab19 | |||
| 06f4cdc649 | |||
| a38d547200 | |||
| 336a7d5b56 | |||
| a15f431a7f | |||
| a0f464830f | |||
| 3bb5b66e8b | |||
| 9bf7fa4081 | |||
| d9ff3242c6 | |||
| 96ead65774 | |||
| cd562ab8ed | |||
| 7ad41927aa | |||
| 3a8a69ac2e | |||
| 4ca9dfd9c0 | |||
| 0d5b4936bd | |||
| 8a9f386d8f | |||
| b5f6eaf159 | |||
| adfee8bf58 | |||
| 25c80921e0 | |||
| fbfa2a71a9 | |||
| 5c51ed48c3 | |||
| 9a1368ef17 | |||
| 22febfc42a | |||
| 31b02b97d3 | |||
| de482dd9f4 | |||
| 42da38c5c3 | |||
| 74af67a41f | |||
| 0a01b55713 | |||
| 170b41441b | |||
| 3b292c2a12 | |||
| 4991e50e16 | |||
| db0ba0d9a0 | |||
| 1f236ef929 | |||
| 3a23ff6b42 | |||
| 7f86c5984d | |||
| 1e9c5adb0a | |||
| 6fcee49b19 | |||
| abab76ccc6 | |||
| ac527851a0 | |||
| 6efd92806f | |||
| 64bbeaedcb | |||
| cfe333e89f | |||
| 3e984a1bcb | |||
| a7237fe62f | |||
| 79165214d7 | |||
| c3c454b7d7 | |||
| 3f7855dd6e | |||
| d4d708d44b | |||
| f0610281e5 | |||
| 7f0b6a3a46 | |||
| e87e7e2164 | |||
| c2a7c089d2 | |||
| 1a3ad390fd | |||
| df5bd4df60 | |||
| c383367e9f | |||
| 79b6010104 | |||
| 7a9f45c96b | |||
| 97b0a98793 | |||
| 7277cb289f | |||
| 5230f90540 | |||
| 86b2d5eff2 | |||
| 803db4e895 | |||
| 7873847616 | |||
| 7cee9f2ebb | |||
| 5e46f149d9 | |||
| 8be9a21efd | |||
| 574fc52332 | |||
| 6a3e26b566 | |||
| 8c924ca98f | |||
| 0355c37bef | |||
| a9adc0c1a2 | |||
| 9b7ee538c4 | |||
| ed286c8894 | |||
| d900a3d08e | |||
| a55da15202 | |||
| cdf5b66729 | |||
| 9728118f94 | |||
| 1cff4b63cd | |||
| 9cc2ea412b | |||
| da14309ef9 | |||
| 3f1ad4b7dc | |||
| fbb216fe3b | |||
| 56c225bf20 | |||
| 95efbd5659 | |||
| 8b68bfb28c | |||
| 4596c1049c | |||
| a5299b52d4 | |||
| b35d95f0c7 | |||
| 9e9bc52737 | |||
| 01419df998 | |||
| 76106e4504 | |||
| a6c00c42fa | |||
| 4e23005b5b | |||
| 4cc9db7115 | |||
| f27e2de220 | |||
| 4f1ed54059 | |||
| eefcabcff3 | |||
| 8227a73e35 | |||
| 63e4669e3f | |||
| adfd8c1939 | |||
| 7d5428f775 | |||
| 8eed7ff534 | |||
| ab3c4562fa | |||
| c79c4e74d0 | |||
| 13f89bf335 | |||
| f1855fd0a1 | |||
| e6a18445c3 | |||
| 1f964c74e9 | |||
| c8ab209426 | |||
| 4fb2c5803c | |||
| 360fea4085 | |||
| b5947545cb | |||
| 9794d67eaa | |||
| 342b76f666 | |||
| b60a639312 | |||
| 49b5906bc7 | |||
| 870706c4ff | |||
| 3075bfb7fc | |||
| 491471fa10 | |||
| 82e06fad33 | |||
| 6dbf61d4e4 | |||
| 4a9028747b | |||
| 81545d192b | |||
| 4a8ff0ccf0 | |||
| 37cdc26160 | |||
| 99341f0484 | |||
| fb6a35ebe4 | |||
| f58ac29ad0 | |||
| f7a565bb80 | |||
| 7060edb3e5 | |||
| bf5e72b7e0 | |||
| 41ae411f9b | |||
| ec6186596d | |||
| 79b7fee47c | |||
| dece19cec6 | |||
| 0044bf10af | |||
| 6aa1d27711 | |||
| e9348d3611 | |||
| 264e77f383 | |||
| b9236e09a7 | |||
| 0cbc284e42 | |||
| 09b38d5f42 | |||
| 9e42a334fa | |||
| 7bb539a06e | |||
| 49438d2ee1 | |||
| 5cdada8265 | |||
| 735aa24020 | |||
| 4147c217b1 | |||
| 14f63a203d | |||
| 8dda639b23 | |||
| e11c0a3633 | |||
| 8487d2c9eb | |||
| ca83f139f7 | |||
| c5e583b215 | |||
| 58e44b7d12 | |||
| 549f618cff | |||
| 8f47474edd | |||
| e9a3510346 | |||
| c5200aada8 | |||
| 30e6e963b3 | |||
| 4e39b93673 | |||
| c72d963f45 | |||
| f215643f2e | |||
| 172d498618 | |||
| aa72be6ff3 | |||
| 313993532e | |||
| b567e56b60 | |||
| e53db3582c | |||
| 17431c1707 | |||
| 72c6bd3f77 | |||
| 7c9ebef5e9 | |||
| ca8b349df3 | |||
| 72b0e11543 | |||
| 1b206c3640 | |||
| 9e2af9dbca | |||
| c60276fc9f | |||
| b7cc987132 | |||
| d00a3167c0 | |||
| 8476c73b0c | |||
| 6b1cd8c30c | |||
| e76b49afd2 | |||
| 46f12dc9ad | |||
| c712b95e0d | |||
| a3e1d8ae21 | |||
| 1ee1a8d6d9 | |||
| 72a066b93e | |||
| f01c764589 | |||
| 0327a829ac | |||
| 6df66d150b | |||
| 882e9b8819 | |||
| 6132f94eff | |||
| ef58cfadaa | |||
| ac5e67ea73 | |||
| bf958d6113 | |||
| 143d2b44d0 | |||
| 71611273d7 | |||
| ef130fe377 | |||
| b27c654311 | |||
| d7d1e2100c | |||
| 90930ea9f9 | |||
| af5afd7700 | |||
| 1ab2185ff1 | |||
| a64df5cf0c | |||
| 0f2f978d4c | |||
| 3866319373 | |||
| f61963b0b0 | |||
| ec5ad809f2 | |||
| 2aa413960d | |||
| 268acb174d | |||
| aa4bbba5ec | |||
| 3733877be1 | |||
| eba61fea2d | |||
| 6d5579a8d6 | |||
| 34e3455128 | |||
| 306cd2f945 | |||
| 07dca3e739 | |||
| 5d4dd1e66f | |||
| 4cb4b145f9 | |||
| 5f371bdc4a | |||
| 1ed417cb69 | |||
| aa58ba392e | |||
| 6cf91a84ca | |||
| 0485610818 | |||
| 0b566980fc | |||
| 7cb8becc09 | |||
| f86176b342 | |||
| 8fa535a01b | |||
| c700b32670 | |||
| 5f202e03a2 | |||
| 22641b452a | |||
| ef2dfe330b | |||
| d3fbb8c19e | |||
| fcd86fbebd | |||
| e3bb69ff10 | |||
| 960d294aa2 | |||
| 770360c614 | |||
| 27f8e63f3d | |||
| f302a0478f | |||
| e8d7ad58be | |||
| a88697b43a | |||
| f4d537e0f5 | |||
| cc6f140812 | |||
| d6c93dd537 | |||
| 424f2b3bdc | |||
| aa5fff47de | |||
| ec0c13a600 | |||
| c920d8423d | |||
| a1f03bec4c | |||
| b8fcfe2e7c | |||
| b5bd4a5e0e | |||
| 072c96e1ec | |||
| 7c2e49bfdb | |||
| 38c8ece642 | |||
| f80fe6d041 | |||
| 9bf1576fb4 | |||
| 72f80a96bc | |||
| d648e177c4 | |||
| 2de655a1cf | |||
| e8f62af7f6 | |||
| da2bd4a501 | |||
| e746aafa2f | |||
| e0aa62c40d | |||
| 71f7a1b166 | |||
| 9d26a892d1 | |||
| 678f570a90 | |||
| 4ece7f2847 | |||
| 4bc1f195db | |||
| 32368caf1b | |||
| b2ff49ee94 | |||
| e91f54e79e | |||
| d9558f13ad | |||
| bb8f4c57c4 | |||
| 8dc8f2567e | |||
| 43bfac99b6 | |||
| 316636f83c | |||
| be379b6d63 | |||
| e3c2fbf82a | |||
| 17f3c9b840 | |||
| 1b6175e598 | |||
| 24de97fac2 | |||
| 26dc479596 | |||
| bf27b44fee | |||
| dca25dbb78 | |||
| 1802b4fe4d | |||
| c55275872e | |||
| 241a5c7bc9 | |||
| d4d1fb26f6 | |||
| 557d547bf1 | |||
| a48cb7bc34 | |||
| 2e7b75affb | |||
| 8af6e9290f | |||
| bc21a1d443 | |||
| 14b277d813 | |||
| 3fc9e10a24 | |||
| 20915038a3 | |||
| 5fa1aa2060 | |||
| 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 | |||
| ff4b267858 | |||
| cbbcd4c571 | |||
| a590d0497f | |||
| 5ad9adeb4a | |||
| ac30d906f0 | |||
| 13c9594be0 | |||
| 5bc071e038 | |||
| e4f5f78bfa | |||
| 88b956cf98 | |||
| bfa0208692 | |||
| f725cf4661 | |||
| 64994cdeda | |||
| 057cc1e8a6 | |||
| 9796a841d9 | |||
| de122735b8 | |||
| 404b493d23 | |||
| e87ede981c | |||
| c04f9eb993 | |||
| 606fb498e1 | |||
| 825da42208 | |||
| a0c06e40a4 | |||
| 721a8f1ecb | |||
| aba8f57279 | |||
| 586a174d6c | |||
| 960286a350 | |||
| bec6966542 | |||
| 8c93fa51f6 | |||
| b1f4e1d4d8 | |||
| cb0e7d64ff | |||
| d7d64e8259 | |||
| 8e7413da97 | |||
| 08b498fccd | |||
| a36f14eb94 | |||
| ecc5a52232 | |||
| f2f9f6e488 | |||
| 7327b90255 | |||
| 85068b8ca2 | |||
| 6ce7a9c16f | |||
| f2cfcfeefc | |||
| eb2658a91c | |||
| 755273a898 | |||
| 9f59a18756 | |||
| d4a24a0f1d | |||
| ef06f0da98 | |||
| 92281fcbb7 | |||
| 4dec0e2c69 | |||
| 636db4afcc | |||
| 5f36f1af11 | |||
| ba25b8755e | |||
| 5a1a596098 | |||
| 6399d13a49 | |||
| 7a25c6e062 | |||
| 06fa54fd25 | |||
| a73ed71b9d | |||
| a335b965d0 | |||
| 2abbb95ea2 | |||
| 725adaa7d0 | |||
| 1f80ba9463 | |||
| 7e7e81e974 | |||
| 4327fdac12 | |||
| 8cfe6bfc17 | |||
| a6f05a5874 | |||
| 33de83f2ac | |||
| 3fb74a1598 | |||
| 3f856afec8 | |||
| 2ac44cdeb6 | |||
| 4e4dc4cb73 | |||
| 855a953010 | |||
| 02a9c422fe | |||
| 3264fda06e | |||
| ca69341024 | |||
| 2c7d472069 | |||
| 169bf069ce | |||
| 8182e6797f | |||
| 1bee0ab04d | |||
| 99cb48ce88 | |||
| 440d91dd0e | |||
| e391bfca80 | |||
| 8168e246a8 | |||
| 0181ad3715 | |||
| 2ef07574ae | |||
| b95dff0751 | |||
| 37392f2bb2 | |||
| 668d4c9c64 | |||
| a80cd3848e | |||
| 66c0d1b2f7 | |||
| db6ed84451 | |||
| bef0996375 | |||
| 4463cc5963 | |||
| 391a1e4cf0 | |||
| d316158fe2 | |||
| 38facf517a | |||
| e02a8d7586 | |||
| 5c9b1e8764 | |||
| 9988dff885 | |||
| dbcf14b566 | |||
| 35ef5674ff | |||
| cc59f0c761 | |||
| 976da45bce | |||
| f263fc067a | |||
| c83ac48bd2 | |||
| 6635b88baa | |||
| 3d159a833e | |||
| 87ea2a611f | |||
| 4b09878bdd | |||
| ea8a2135cd | |||
| b0162e6a92 | |||
| 76d86395ef | |||
| 8ab15e5dc4 | |||
| e268870636 | |||
| d2ac807252 | |||
| 49e9f41ef2 | |||
| 0af01f6f1f | |||
| 68cda968a1 | |||
| 013b319fab | |||
| f6826fcefc | |||
| 2899ba5949 | |||
| 56a91db7e7 | |||
| a558b7e104 | |||
| 8f4d94f046 | |||
| 7a833e2233 | |||
| 9446bf5f98 | |||
| bf65746d00 | |||
| 23e353b18b | |||
| f08a7862de | |||
| 46daaa4ba1 | |||
| 023a2c2f09 | |||
| f66d59531d | |||
| 1bcd0f4c1a | |||
| b76cd1d5ae | |||
| a0f3bc8ccb | |||
| 55ba0d08a5 | |||
| dea72738c1 | |||
| cc78780a8e | |||
| a1d1fe7763 | |||
| 4a00809061 | |||
| a39ed9764c | |||
| cd760bbd84 | |||
| aaa5ba99aa | |||
| 84bdc6be7f | |||
| 2113508b6d | |||
| 8dd6bf8933 | |||
| 7fe4212684 | |||
| 08f170d217 | |||
| 8bdda64794 | |||
| 20da6a1c20 | |||
| ec08c24dca | |||
| 8586931630 | |||
| a992a5b3b3 | |||
| 667fc79a6f | |||
| 0f05970141 | |||
| ac7430dded | |||
| e5e762efcd | |||
| 9c73e16560 | |||
| b3d0c1ef9c | |||
| c605e222a7 | |||
| 397078f7ff | |||
| 578f26a738 | |||
| 3ad8065e20 | |||
| 24218e7570 | |||
| 66c7717f04 | |||
| 16f946550e | |||
| 412f8ecc6c | |||
| 9960a31485 | |||
| 51dcf642b3 | |||
| 437632f859 | |||
| bfeea555b2 | |||
| 98f0f442eb | |||
| 479f94c372 | |||
| 3b999dc5e2 | |||
| 0140713e86 | |||
| 20943ad3d3 | |||
| 15b2ec9721 | |||
| 3b1544b5e4 | |||
| c9cd082855 | |||
| b317d597ac | |||
| d7c002890c | |||
| 3bb19811e7 | |||
| 348dd22279 | |||
| 065a01de48 | |||
| 3e99b4cbf6 | |||
| 09fb2f6557 | |||
| 6968da3ac7 | |||
| 9ca97bf4bc | |||
| bf1c1b84c3 | |||
| 0c59503edb | |||
| c70314d930 | |||
| 6fa08861f8 | |||
| 9104ca8e49 | |||
| 72b95151a1 | |||
| 2af33b3630 | |||
| 378e6ec9af | |||
| 654e795545 | |||
| 27b68c1174 | |||
| c62ba2451e | |||
| 1ae8eabe99 | |||
| d72d1b8a99 | |||
| 72726c1cf9 | |||
| b939d6016b | |||
| 6203d9a150 | |||
| 36a2626ccc | |||
| 861f11af66 | |||
| bd057a4cc9 | |||
| 39fdfae541 | |||
| dc24a8c781 | |||
| 9f57fb1421 | |||
| 59fa21779b | |||
| 059f57db2d | |||
| a140671aad | |||
| 13c8be8d06 | |||
| 5fe8990fb4 | |||
| e0dbfb0488 | |||
| 12799b7159 | |||
| 722ed128a1 | |||
| 9929746b1d | |||
| cc628821e6 | |||
| d70035ff0c | |||
| 857af61af1 | |||
| eec90274d8 | |||
| f6fc484cb3 | |||
| e8fff55c42 | |||
| ba07946f1e | |||
| 3cf3cdd705 | |||
| 600f668c63 | |||
| 9801fce659 | |||
| 8a8990b12b | |||
| 4c1f51110b | |||
| fe85c1bbb9 | |||
| 913d538587 | |||
| a89ff72308 | |||
| 9e704365fc | |||
| a99e58184b | |||
| 485bdbc56a | |||
| d981bc8fd0 | |||
| 7000168fd4 | |||
| ff22480d89 | |||
| 5694f97a6b | |||
| 8ec85e2829 | |||
| b677d3fac7 | |||
| 706aacd8e6 | |||
| dc6719cf54 | |||
| 803bc2d9fc | |||
| 7de5b55091 | |||
| 7cc67cf8f0 | |||
| 76c5101092 | |||
| 9909c3a0ed | |||
| 2f8d2f4854 | |||
| 60a3751839 | |||
| b1ee34ba0c | |||
| ce87e0c40b | |||
| 069ad6a09a | |||
| 71cd548c00 | |||
| bf1403c818 | |||
| 1ddd05302b | |||
| bcc622a24d | |||
| ea3301c75c | |||
| a06a81a415 | |||
| 4b1c4f7ccc | |||
| d1950acd01 | |||
| 21f2622a4b | |||
| 039b70eed2 | |||
| 79f6a43019 | |||
| d8e4308b1b | |||
| 3d75093b2c | |||
| 434fbb3463 | |||
| 023dc89c3e | |||
| de3eb8969c | |||
| b5641be30d | |||
| fbd6eac877 | |||
| ff4515bbf1 | |||
| 1fecab177b | |||
| 5dc0fe05af | |||
| b1b385c455 | |||
| 25bba912f6 | |||
| 3c6e86d04b | |||
| cb6e323596 | |||
| 3d2035d08a | |||
| cf4b04e047 | |||
| da86f916d8 | |||
| 243b5be31c | |||
| e7a07f7e92 | |||
| c3d753cf38 | |||
| b01e6387fc | |||
| 70c46d098b | |||
| d86aca0f5d | |||
| 816e4a096e | |||
| 09414fe36a | |||
| 1d1e819817 | |||
| df0e7508db | |||
| 342daf1be8 | |||
| 92b1f01118 | |||
| f6bb9b582a | |||
| 8fb8bd932b | |||
| aad62bc976 | |||
| 3f74b94784 | |||
| 38a49a7f37 | |||
| e9467341fa | |||
| f603bf6be7 | |||
| 131e051ddc | |||
| 6cc05e9e27 | |||
| f626fe3166 | |||
| c00e67542f | |||
| 6bc57b6132 | |||
| 22660362a3 | |||
| d972e97c88 | |||
| b1ab9975b7 | |||
| 3991f4daec | |||
| 7ca89d8b54 | |||
| f6b567d6fc | |||
| 998b07ab8c | |||
| 8addba8203 | |||
| 4936896ff7 | |||
| 3ab930a107 | |||
| 18b7484c5b | |||
| de512a5ea2 | |||
| 6754c8e85e | |||
| 113cfae2dc | |||
| 57243ff010 | |||
| 33aebf9cb5 | |||
| 7c4dfe96ee | |||
| 6e58ddf681 | |||
| bf19120c27 | |||
| cae5c049e4 | |||
| 3d37087916 | |||
| ff76e4bd89 | |||
| 6949f1328c | |||
| a0a506a3c4 | |||
| b63090c20f | |||
| aa5a4a9977 | |||
| b15ebda948 | |||
| abf4f061c1 | |||
| 252eef2e5e | |||
| 245cd3ee1a | |||
| 3bea8f9706 | |||
| 45cb29d9a0 | |||
| dfe808bee7 | |||
| d974b1ff0e | |||
| 58f3dd5336 | |||
| 56269170cb | |||
| b00f16648f | |||
| 4290c4ca22 | |||
| fc66c31e89 | |||
| 7f7c8e831e | |||
| a26c849532 | |||
| 8f057ca9d1 | |||
| d2991e60b6 | |||
| 4a56621ec3 | |||
| 10ba1430f9 | |||
| a398e7a550 | |||
| 6d71f24f75 | |||
| 96816c12ca | |||
| 8f4d20e411 | |||
| 9984926f69 | |||
| cf758d773e | |||
| a2a6081027 | |||
| c012f0c4c5 | |||
| 5a10ed37a7 | |||
| 9e85900057 | |||
| 1a9dd9de0b | |||
| 68e3b787f9 | |||
| 0dae5bef71 | |||
| 6706704ad6 | |||
| b4413ed726 | |||
| 1067fa015e | |||
| 5e1fe88b8b | |||
| 411335ebcb | |||
| 91ed41b536 | |||
| f19c15491e | |||
| 024c0032eb | |||
| 0da81a4d10 | |||
| 4a9f7e3bce | |||
| 049839a5c7 | |||
| cf4dcc34ec | |||
| a901927844 | |||
| 4d612c15af | |||
| 07ab0bc5a1 | |||
| 8aec87cc02 | |||
| a6025e6fab | |||
| 442e411cde | |||
| e841a61bf0 | |||
| acec0194de | |||
| 2f1c2110c6 | |||
| 8557f5b94a | |||
| 8e573eea4c | |||
| babef8baae | |||
| 149765764e | |||
| efd4ab46f5 | |||
| 45d6444c7e | |||
| ae8239e5de | |||
| aa65f49190 | |||
| f0994ba457 | |||
| 1bda41ff67 | |||
| dae91ed243 | |||
| 5a799139cd | |||
| de42a428e6 | |||
| eb36d0742a | |||
| 63c7041e1f | |||
| d8a9123852 | |||
| b1263ddc69 | |||
| 19031c2197 | |||
| 7e50e17aaf | |||
| 7a2ffdf39c | |||
| a7265c4251 | |||
| ce8fa79206 | |||
| 6f39f639bd | |||
| b05c63549d | |||
| a7db123437 | |||
| 9f98660423 | |||
| 241c714a8b | |||
| 7b0fa862d2 | |||
| 67ac3cfe32 | |||
| bfe2d4d573 | |||
| c926e0afcc | |||
| 26950c5673 | |||
| 5bc07e6d57 | |||
| 86788362a5 | |||
| c3666a9a71 | |||
| deedc5fb2e | |||
| 23b5ffa97d | |||
| f4fbe67db9 | |||
| a2c7a75705 | |||
| 599ce0eade | |||
| d68f2ef12c | |||
| 11f3ab8dc7 | |||
| 67d30353f0 | |||
| 75b4a6dd46 | |||
| 4813163eac | |||
| 86bbea337d | |||
| 5c5210625e | |||
| e63a30064b | |||
| a4a1eec30b | |||
| 222b1ddbd9 | |||
| d35164506a | |||
| a88dd88a07 | |||
| 1ed08f01ea | |||
| a47c01fd41 | |||
| eca07ab830 | |||
| 35e385f7a7 | |||
| 3512715704 | |||
| 191e3b7d2c | |||
| 6d07881141 | |||
| 13fbbc190a | |||
| 251fe626f2 | |||
| 61c6a6e7f3 | |||
| 5fee3a9288 | |||
| 911f218f0b | |||
| 9b68d8101e | |||
| af41350eb9 | |||
| cfe6f27d48 | |||
| 8f63e348b3 | |||
| b314dd0900 | |||
| 8901cf8e81 | |||
| 950fab6374 | |||
| e11736e1d2 | |||
| 9d1f5c42ce | |||
| c062fba69c | |||
| a84046390b | |||
| 4628095fe7 | |||
| aa29323a8a | |||
| 787ed9bc0f | |||
| d5617b7c3a | |||
| daaff8974e | |||
| 1ef60a9e5e | |||
| 4fcbd511c0 | |||
| fb6e395ad8 | |||
| 64d6bff6d9 | |||
| d9216060bc | |||
| 998cbace92 | |||
| bcaa9a92e5 | |||
| 2204038951 | |||
| 576adc9036 | |||
| 998f67ac5d | |||
| 00de18be9a | |||
| be981a2c9b | |||
| c61d32816a | |||
| da3cf0d4a3 | |||
| f3fbb0b89c | |||
| 7124b9a36c | |||
| e311a39632 | |||
| 712a6b6c39 | |||
| 51407abe44 | |||
| ed0a1c57d6 | |||
| 8a470b1038 | |||
| 7858228505 | |||
| baddabaa16 | |||
| 2525a22d78 | |||
| 427b434ce3 | |||
| 7b4ef8fc31 | |||
| 5f921965e6 | |||
| ce14cd02e4 | |||
| 1e705c8ed5 | |||
| 05f501af52 | |||
| b8ae65bb30 | |||
| e94fa4844f | |||
| 321e2087ea | |||
| e0965aae5e | |||
| aac60edce2 | |||
| 414c1de963 | |||
| 9dc9a6923e | |||
| b589102be8 | |||
| 7ca4dfe09b | |||
| df367e0d47 | |||
| c584b82ddb | |||
| 680219ebcb | |||
| 5f17ab2501 | |||
| ef87487f60 | |||
| c84e912dd8 | |||
| 8bafe72434 | |||
| 2ebff2623f | |||
| 76dcc69e44 | |||
| 72418ce4d7 | |||
| 682ef22194 | |||
| e221b1eed4 | |||
| ee8dd41605 | |||
| 696306f066 | |||
| d0b8d666e4 | |||
| 1807d5b5d4 | |||
| 4a81826d19 | |||
| 85c12aa322 | |||
| 848a10c3a8 | |||
| da9d0dc3bc | |||
| a4baa29678 | |||
| daaca822ac | |||
| a9b5280691 | |||
| 59ced3f947 | |||
| 264706210b | |||
| 22ae7dd1f3 | |||
| 7677ae254f | |||
| 1816e9d5cf | |||
| 7ccb4c5f06 | |||
| 9be6755f65 | |||
| 01205af018 | |||
| e5fb986463 | |||
| 5b5150e6d4 | |||
| 55d24e577e | |||
| 9d8e3f5049 | |||
| 7f50fa3fcf | |||
| 99ede83bdc | |||
| fff8b78aba | |||
| 61488e840d | |||
| 7ca1989d98 | |||
| a80d01209c | |||
| 4595dcb7ed | |||
| d2a8d655c8 | |||
| a688d3feb5 | |||
| 80e2b34abc | |||
| 7d1d88a32f | |||
| fb3d43d2d5 | |||
| d95c048edd | |||
| ebdec4fa44 | |||
| df2fc9d77c | |||
| ead8dbbaa5 | |||
| d7e815d2bb | |||
| 95615a5501 | |||
| f58b0a65f0 | |||
| 75487b1f58 | |||
| b59ad521ca | |||
| c7daaa00cd | |||
| b47ff975b0 | |||
| 606f24e981 | |||
| d043a87b30 | |||
| c83d9287a7 | |||
| 4cae7525d9 | |||
| fa8092b2cd | |||
| 76966d2ce7 | |||
| b2b07a2be6 | |||
| 5a740aecb0 | |||
| a21628e350 | |||
| 1ae79331e7 | |||
| 9fcd686fda | |||
| 8b14e141d0 | |||
| 1759fd4cf9 | |||
| 9cbc6c91c4 | |||
| 12e7837602 | |||
| 21c3a419a5 | |||
| ff349f2edf | |||
| 287fac3a89 | |||
| 088d8ca207 | |||
| ba206bb387 | |||
| c37902d3a4 | |||
| 4fc01f3f7b | |||
| da38684cdd | |||
| f5ed71bcc6 | |||
| cb2cb72333 | |||
| 8fc26183e9 | |||
| d53c8f9830 | |||
| e8ae8fddb7 | |||
| c8136d4231 | |||
| b876867297 | |||
| e4b8380530 | |||
| 91dfd59731 | |||
| fbd599194c | |||
| 5fdff90a10 | |||
| 0270ec26fb | |||
| 96c62619e6 | |||
| 79e342be57 | |||
| 083155413d | |||
| c89f23b018 | |||
| d83019cbe4 | |||
| f971ec5d34 | |||
| cc7271aa73 | |||
| c5776ce41f | |||
| f873d6b375 | |||
| 75c5ebbffa | |||
| c86169022a | |||
| d51a724ade | |||
| db0a79da93 | |||
| c1143d7a6d | |||
| 48393e0e83 | |||
| f3697431a4 | |||
| 7b4730271d | |||
| e3d6e5f420 | |||
| 9cbe36d4c6 | |||
| af6f7a7146 | |||
| b25bb2cc53 | |||
| c18d95e89a | |||
| 79ded6018b | |||
| fa42d03a65 | |||
| 59f316b341 | |||
| 22f7de2c09 | |||
| f307b8ba7a | |||
| b4b9df81cb | |||
| 5034a20345 | |||
| 2a71c2b0e7 | |||
| 26944f9e39 | |||
| c37b3d3df5 | |||
| e64946c3b6 | |||
| ae135b89d6 | |||
| e0a62d9b35 | |||
| 4112426848 | |||
| 39dbffd8d0 | |||
| 638b399b31 | |||
| 952d6183ed | |||
| 8a5713ef7c | |||
| 3365a6008d | |||
| 382d7bfaa1 | |||
| 2e13ddf405 | |||
| 3daf2f7738 | |||
| 1d3acc8ed3 | |||
| 944d35971d | |||
| fa341bab30 | |||
| 9ce8af0b82 | |||
| 036a6e3e41 | |||
| d825b9ec26 | |||
| f4c6ca4554 | |||
| 9afe4aea4b | |||
| 327929243c | |||
| 3cc8c3284a | |||
| f4349c7a8c | |||
| e2c18c4e1e | |||
| 4b46d847f0 | |||
| 81fe768f6a | |||
| c3f016eae8 | |||
| 8923779ab0 | |||
| ebd3ef842f | |||
| 583218a045 | |||
| 18c033d57f | |||
| 153b2bfa53 | |||
| b676f80110 | |||
| 0a9b325360 | |||
| f7fbaa534d | |||
| af9e6f2c46 | |||
| ea93a22e14 | |||
| 0e31726fd3 | |||
| 9f7e6778c5 | |||
| 590d9c2e61 | |||
| 6c31a2bfa6 | |||
| c12fe9f25e | |||
| f943669e18 | |||
| 9bd83f3acf | |||
| 3b26735998 | |||
| aada15fc41 | |||
| 79d25769ee | |||
| 6a2d383be6 | |||
| 1dd6800987 | |||
| 545c8fa482 | |||
| 5e673a9ee0 | |||
| e3d18643b8 | |||
| 92eb67a2af | |||
| 2f318bbe9f | |||
| b1bed59be2 | |||
| fac7eb61b3 | |||
| 0ac732a3a3 | |||
| 4450d03a54 | |||
| bf3f68fa19 | |||
| e4e7868b30 | |||
| 46a551df16 | |||
| 3d7fa680cb | |||
| 20a12462b1 | |||
| c38035e25e | |||
| a49fb1940e | |||
| 8d4fdaf902 | |||
| 32774d23c7 | |||
| 2820adad53 | |||
| 7ecd7eeba1 | |||
| f7a427d2c0 | |||
| 0cc9cf8b45 | |||
| 59ed8c9660 | |||
| d06f94bddd | |||
| 71562ab0e5 | |||
| b5955f08c9 | |||
| 278ddf037f | |||
| c120569894 | |||
| ac3151af92 | |||
| aa376f1737 | |||
| 1189ad7862 | |||
| 0f8a0f89e3 | |||
| 88c13e7b9a | |||
| 68dc261b44 | |||
| 596baac62e | |||
| 4cf3af0c7b | |||
| 5a8503ec14 | |||
| b99b6735d9 | |||
| e44f95724d | |||
| 52189b7880 | |||
| dec0d0dea7 | |||
| 3dbeb1ccb6 | |||
| 46f96e94ec | |||
| 5a0f272fa8 | |||
| 89b30bcf58 | |||
| 6561b99f8f | |||
| 473d8f5f85 | |||
| 329e3eee21 | |||
| 08d8d65599 | |||
| 07049c9afb | |||
| 2964c1d82e | |||
| 36c5dd7eaa | |||
| 75ab8552d9 | |||
| b84039b506 | |||
| b78d5b3d03 | |||
| fab43097dc | |||
| 064081c771 | |||
| c8998ba294 | |||
| 3525d35d15 | |||
| 40b2466adc | |||
| 9d9ee7c585 | |||
| 35fedbe817 | |||
| e65c32c4c7 | |||
| 827acdd3f9 | |||
| 113769791f | |||
| 6c76086916 | |||
| 5d2a1d21d5 | |||
| 373370fde5 | |||
| 6e40f92aaf | |||
| 2165ba3406 | |||
| 302cb8a5be | |||
| b0e02b43fc | |||
| 0b27890484 | |||
| 2107c13b3d | |||
| 5db247e632 | |||
| 5f41aecc8d | |||
| c1155a4338 | |||
| 6840a13370 | |||
| da806b9492 | |||
| 8f1e28c0ab | |||
| 3d6d46b30d | |||
| 7903eed284 | |||
| 57c69738ba | |||
| 0d49ea0d41 | |||
| 0c8157dbc0 | |||
| 2ee4db5e48 | |||
| d7b278f2f7 | |||
| 48c4789505 | |||
| 18d74f1057 | |||
| 4e65a5b1a1 | |||
| e04856f794 | |||
| b09d23f97f | |||
| d7bbfb0fc3 | |||
| 3529649ba9 | |||
| a0857817de | |||
| fdd659f393 | |||
| 8d39234fd0 | |||
| 9eb8da2789 | |||
| 0fce1c0fd8 | |||
| ffb1ef0470 | |||
| c23c285f82 | |||
| 862c6aea43 | |||
| 81b560d791 | |||
| 54fe4b7588 | |||
| 55459a4f42 | |||
| c6062ee70e | |||
| 9eafd3e6ca | |||
| bed184dc1f | |||
| e7ac26ff5a | |||
| 29094ba3b3 | |||
| fad78641d6 | |||
| a18188876c | |||
| a32cb0142b | |||
| 4faee3e48e | |||
| 4a3c133152 | |||
| 1a6afcd266 | |||
| ae79c34508 | |||
| f567831d92 | |||
| 7eb69c1ad7 | |||
| cf36ca4285 | |||
| 8aaff0af89 | |||
| 0e4ae01498 | |||
| 4b77280c65 | |||
| 7b90f8cb13 | |||
| a0df84e4f2 | |||
| c33215529a | |||
| 8876e6a098 | |||
| c5be114db2 | |||
| 9bee2a90f9 | |||
| cab955c292 | |||
| cd6a811bbd | |||
| ca8c8e6490 | |||
| 84f9a83f55 | |||
| 253951b4b3 | |||
| 2efc669ab2 | |||
| 4d6444ebf3 | |||
| 5444ed77ad | |||
| 94d8d8a9d4 | |||
| dd71fe80a5 | |||
| e02badf7bb | |||
| b73092eb64 | |||
| dd88622c64 | |||
| aa28f87b0c | |||
| c4d7126c4d | |||
| 1e1bcd4a30 | |||
| 86bc063941 | |||
| d24b3c46bf | |||
| dce85eb519 | |||
| 37222f07d9 | |||
| 4ab879d697 | |||
| 49646a79e5 | |||
| 681e52df50 | |||
| 9940e1210e | |||
| fb554c0315 | |||
| 10892812c3 | |||
| accf8eeb77 | |||
| d8ff5987dd | |||
| 3e41edd3b5 | |||
| d014d418e9 | |||
| 9126cfff20 | |||
| cc1b56501d | |||
| 9806d5ff4c | |||
| a5ad9648bf | |||
| d1d13a72e4 | |||
| 3630099234 | |||
| 00c520d066 | |||
| 941a24c75b | |||
| 797ff66474 | |||
| 50bdc01484 | |||
| 9d51a478b9 | |||
| d9e340ddc4 | |||
| 1d4179df75 | |||
| beee1e91d6 | |||
| 917b6012e8 | |||
| 35935d2bac | |||
| da14632794 | |||
| 5d0b0ad33e | |||
| a868a8a8b7 | |||
| d1a6ac531f | |||
| 5037df744f | |||
| ae255a3bd9 | |||
| da88a501ad | |||
| fa733afa51 | |||
| b9885e8de4 | |||
| b60c723457 | |||
| 22efe81080 | |||
| 2bb7ff3c13 | |||
| 2926717aef | |||
| 68b4cf00e2 | |||
| a49d54d66c | |||
| 440169ff60 | |||
| ce0267e25b | |||
| 1fc361242e | |||
| 9088d22a66 | |||
| 72cc6f3d75 | |||
| 1ff32d5d0a | |||
| a2b1924e00 | |||
| adf6916598 | |||
| 7e926167f0 | |||
| 31c14bf748 | |||
| 32484bd32e | |||
| 5395385d1e | |||
| 04ade1008b | |||
| 0035da548b | |||
| 4603de0e70 | |||
| 9bceaade05 | |||
| 12c1ff65e9 | |||
| 3194becdad | |||
| 7b8cbf7f86 | |||
| 6174b17c24 | |||
| c198e83f70 | |||
| 53fa4a20e9 | |||
| e60863a290 | |||
| 43c1de51f5 | |||
| 946563d3b2 | |||
| 7eb8c5ec35 | |||
| 3f2ef1d54e | |||
| 296bf63196 | |||
| 0d2b60b905 | |||
| 6c65a21692 | |||
| 161b11e428 | |||
| daf83cfc84 | |||
| f7748d51df | |||
| 871f5d39e4 | |||
| 5d13b4b705 | |||
| 3f91f37aff | |||
| 92e52a2284 | |||
| a08981f876 | |||
| 3efd5fb77a | |||
| 5187a43543 | |||
| c3d62bb8d8 | |||
| 6a733de556 | |||
| c099e843d5 | |||
| b9e9eae93f | |||
| 5f3a5871f1 | |||
| 811f12135a | |||
| f01fdd0070 | |||
| 2c172c0851 | |||
| d20cc367b8 | |||
| 399a16fa28 | |||
| eadb9a733f | |||
| d971e95900 | |||
| 32ac454b5b | |||
| 0b6940b121 | |||
| 2cadd6af44 | |||
| ad0f96fcb1 | |||
| 3c8b5cb313 | |||
| 063b5655f7 | |||
| fcf2387794 | |||
| d03ed6570b | |||
| f9783b4806 | |||
| 1795a891ce | |||
| cbdc532f83 | |||
| 05bdd81646 | |||
| 124554bae5 | |||
| cba54be913 | |||
| 2725536d7d | |||
| da0acfe851 | |||
| beeef7a4f7 | |||
| 133a127d8a | |||
| 7f2ebb6aeb | |||
| 8da9b52eae | |||
| c8e30383ba | |||
| 2700b63887 | |||
| a7d5a6ccb9 | |||
| 17713d05ec | |||
| be3380aaf3 | |||
| 3674d9da85 | |||
| 40a4ab5410 | |||
| 0e6606e469 | |||
| 54a2181960 | |||
| feff1684c4 | |||
| e490e15bc4 | |||
| 81e08e02ff | |||
| 469844d97b | |||
| f593526bd4 | |||
| 892ea29ba8 | |||
| d244ad9983 | |||
| acde1c6742 | |||
| 65a01f4776 | |||
| 07da11d852 | |||
| 5f812ae649 | |||
| 502b8c2270 | |||
| fda811de97 | |||
| af3f7ac810 | |||
| fe7f021ddb | |||
| 935c6caf96 | |||
| 120e54fb29 | |||
| 90bce1d437 | |||
| cd809d17d3 | |||
| 831dd3e2e0 | |||
| e7fd29b9cb | |||
| b8e208eb32 | |||
| c008f33bc3 | |||
| 542c908e30 | |||
| 7b5cacd6f1 | |||
| 08529242bf | |||
| d88219c726 | |||
| 1ae9449d92 | |||
| a3ee7ca2d8 | |||
| 7fd0b1fa08 | |||
| 6f37024e34 | |||
| 111572e3f2 | |||
| 3347b4c990 | |||
| c9875d24b4 | |||
| 6a2122e1ac | |||
| 94a75603c3 | |||
| 4b024e0ad7 | |||
| d55925bccd | |||
| b06a065d44 | |||
| 0b5adcbfab | |||
| 15b77ad10a | |||
| 8d84562f32 | |||
| 180bd2a1db | |||
| 136d159833 | |||
| 06fb7b41b4 | |||
| e3026062d8 | |||
| 088fd14c03 | |||
| 8a69c69b7f | |||
| 1e84332119 | |||
| 197714a57a | |||
| 01d61ab19b | |||
| 567cdc6f8d | |||
| b4569d7fe2 | |||
| be6e8c7713 | |||
| a9ecaed5bd | |||
| 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,6 @@
|
||||
deploy
|
||||
docs
|
||||
api/static
|
||||
web/node_modules
|
||||
desktop
|
||||
|
||||
@@ -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
|
||||
+4
-15
@@ -1,15 +1,7 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
src/dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
@@ -22,10 +14,7 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
src/tmp
|
||||
src/bin
|
||||
src/data
|
||||
web/.env.development
|
||||
web/.env.test
|
||||
web/.env.test2
|
||||
config.toml
|
||||
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/` 中同步记录新增敏感字段与启用步骤。
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
# 更新日志
|
||||
|
||||
## 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 构建,提升构建速度和兼容性
|
||||
- 功能优化:使用 SSE 发送消息,替换原来的 Websocket 消息方案
|
||||
- 功能新增:管理后台支持设置默认昵称
|
||||
- 功能优化:支持 Suno v4.5 模型支持
|
||||
- 功能新增:用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。
|
||||
- 功能优化:修改重新回答功能,撤回千面的问答内容为可编辑内容,撤回的内容不会增加额外的上下文
|
||||
- 功能优化:优化聊天记录的存储结构,增加模型名称字段,支持存储更长的模型名称
|
||||
- Bug 修复:聊天应用绑定模型后无效,还是会轮询 API KEY,导致一会成功,一会请求失败。
|
||||
- 功能优化:如果管理后台没有启用会员充值菜单,移动端也不显示充值套餐功能
|
||||
|
||||
## v4.2.3
|
||||
|
||||
- 功能优化:增加模型分组与模型描述,采用卡片展示模式改进模型选择功能体验
|
||||
- 功能优化:化思维导图下载图片的清晰度以及解决拖动、缩放操作后下载图片内容不全问题
|
||||
- Bug 修复:修复 MJ 画图页面已画出的图,点复制指令无效问题
|
||||
- 功能优化:MJ 画图的分辨率支持自定义,优先使用 prompt 中--ar 参数
|
||||
- Bug 修复:修复 MJ 绘画 U1-V1,拼写错误
|
||||
- 功能优化:支持自动迁移数据表结构,无需在手动执行 SQL 了
|
||||
- 功能优化:移除首页的文字动画效果
|
||||
- 功能优化:在聊天页面增加对话列表展开和隐藏功能
|
||||
- 功能优化:聊天页面增加 AI 思考中动画效果
|
||||
|
||||
## v4.2.2
|
||||
|
||||
- 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。
|
||||
- 功能优化:支持原生的 DeepSeek 推理模型 API,聊天 API KEY 支持设置完整的 API 路径,比如 https://api.geekai.pro/v1/chat/completions
|
||||
- 功能优化:支持 GPT-4o 图片编辑功能。
|
||||
- 功能新增:对话页面支持 AI 输出语音播报(TTS)。
|
||||
- 功能优化:替换瀑布流组件,优化用户体验。
|
||||
- 功能优化:生成思维导图时候自动缓存上一次的结果。
|
||||
- 功能优化:优化 MJ 绘图页面,增加 MJ-V7 模型支持。
|
||||
- 功能优化:后台管理增加生成一键登录链接地址功能
|
||||
|
||||
## v4.2.1
|
||||
|
||||
- 功能新增:新增支持可灵生成视频,支持文生视频,图生生视频。
|
||||
- Bug 修复:修复手机端登录页面 Logo 无法修改的问题。
|
||||
- 功能新增:重构所有异步任务(绘图,音乐,视频)更新方式,使用 http pull 来替代 websocket。
|
||||
- 功能优化:优化 Luma 图生视频功能,支持本地上传图片和远程图片。
|
||||
- Bug 修复:修复移动端聊天页面新建对话时候角色没有更模型绑定的 Bug。
|
||||
- 功能优化:优化聊天页面代码块样式,优化公式的解析。
|
||||
- 功能优化:在绘图,视频相关 API 增加提示词长度的检查,防止提示词超出导致写入数据库失败。
|
||||
- Bug 修复:优化 Redis 连接池配置,增加连接池超时时间,单核服务器报错 `redis: connection pool timeout`。
|
||||
- 功能优化:优化邮件验证码发送逻辑,更新邮件发送成功提示。
|
||||
|
||||
## v4.2.0
|
||||
|
||||
- 功能优化:优化聊天页面 Notice 组件样式,采用 Vuepress 文档样式
|
||||
- Bug 修复:修复主题切换的组件显示异常问题
|
||||
- 功能优化:支持 DeepSeek-R1 推理模型,优化推理样式输出
|
||||
- 功能优化:优化 Suno 歌曲播放按钮样式,居中显示
|
||||
- 功能优化:后台管理新增模型的时候,可以绑定所有的 API KEY,而不只是能绑定 Chat 类型的 API KEY
|
||||
- 功能新增:新增每日签到功能,每日签到可以获得算力奖励
|
||||
- 功能优化:兼容 OpenAI o3 系列模型
|
||||
- 功能优化:API 默认开启允许跨域调用
|
||||
- 功能优化:优化 docker-compose.yaml 配置,增加各容器依赖关系
|
||||
|
||||
## v4.1.9
|
||||
|
||||
- 功能优化:优化系统配置,移除已废弃的配置项
|
||||
- 功能优化:GPT-O1 模型支持流式输出
|
||||
- 功能优化:优化代码引用快样式,支持主题切换
|
||||
- 功能优化:登录,注册页面允许替换用户自己的 Logo 和 Title
|
||||
- Bug 修复:修复 OpenAI 实时语音通话没有检测用户算力不足的 Bug
|
||||
- 功能新增:管理后台增加算力日志查询功能,支持按用户,按模型,按日期,按类型查询算力日志
|
||||
- 功能优化:支持为模型绑定 Dalle 和 chat 类型的 API KEY
|
||||
- 功能新增:支持管理后台设置 ICP 备案号
|
||||
|
||||
## v4.1.8
|
||||
|
||||
- 功能优化:**UI 全新改版,支持主题切换**。 :rocket: :rocket: :rocket:
|
||||
- 功能新增:Gitee AI API 接口接入,目前支持 Gitee 的 SD 绘图接口,支持 Gitee 的 AI 对话接口。:rocket: :rocket: :rocket:
|
||||
- Bug 修复:修复音 Luma API 更新导致任务响应解析失败的错误
|
||||
- 功能优化:支持 Suno v4.0 模型支持
|
||||
- Bug 修复:修复 Suno 已完成任务删除失败的 错误
|
||||
- 功能新增:支持 OpenAI 实时语音通话功能,目前已经支持按次收费,支持管理员设置每次实时语音通话的算力消耗
|
||||
- 功能新增:生成提示词需要消耗算力,支持管理员设置每次生成提示词的算力消耗,防止被白嫖
|
||||
- 功能新增:DALL-E-3 绘图支持 Flux 绘图模型,支持在管理后添加 Flux,SD 等绘图模型
|
||||
- 功能优化:Markdown 支持解析 emoji 表情
|
||||
- 功能优化:当管理后台禁用了某个绘图菜单的时候,移动端绘图菜单也会同步禁用(不显示该功能)
|
||||
|
||||
## v4.1.7
|
||||
|
||||
- Bug 修复:手机邮箱相关的注册问题 [#IB0HS5](https://gitee.com/blackfox/geekai/issues/IB0HS5)
|
||||
- Bug 修复:音乐视频无法下载,思维导图下载后看不清文字[#IB0N2E](https://gitee.com/blackfox/geekai/issues/IB0N2E)
|
||||
- 功能优化:保存所有 AIGC 任务的原始信息,程序启动之后自动将未执行的任务加入到 redis 队列
|
||||
- 功能优化:失败的任务自动退回算力,而不需要在删除的时候再退回
|
||||
- 功能新增:支持设置一个专门的模型来翻译提示词,提供 Mate 提示词生成功能
|
||||
- Bug 修复:修复图片对话的时候,上下文不起作用的 Bug
|
||||
- 功能新增:管理后台新增批量导出兑换码功能
|
||||
|
||||
## v4.1.6
|
||||
|
||||
- 功能新增:**支持 OpenAI 实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。
|
||||
- 功能优化:优化 MysQL 容器配置文档,解决 MysQL 容器资源占用过高问题
|
||||
- 功能新增:管理后台增加 AI 绘图任务管理,可在管理后台浏览和删除用户的绘图任务
|
||||
- 功能新增:管理后台增加 Suno 和 Luma 任务管理功能
|
||||
- Bug 修复:修复管理后台删除兑换码报 404 错误
|
||||
- 功能优化:优化充值产品定价逻辑,可以设置原价和优惠价,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**。
|
||||
|
||||
## v4.1.5
|
||||
|
||||
- 功能优化:重构 websocket 组件,减少 websocket 连接数,全站共享一个 websocket 连接
|
||||
- Bug 修复:兼容手机端原生微信支付和支付宝支付渠道
|
||||
- Bug 修复:修复删除绘图任务时候因为字段长度过短导致 SQL 执行失败问题
|
||||
- 功能优化:优化 Vue 组件通信代码,使用共享数据来替换之前的事件订阅模式,效率更高一些
|
||||
- 功能优化:优化思维导图生成功果页面,优化用户体验
|
||||
|
||||
## v4.1.4
|
||||
|
||||
- 功能优化:用户文件列表组件增加分页功能支持
|
||||
- Bug 修复:修复用户注册失败 Bug,注册操作只弹出一次行为验证码
|
||||
- 功能优化:首次登录不需要验证码,直接登录,登录失败之后才弹出验证码
|
||||
- 功能新增:给 AI 应用(角色)增加分类,前端支持分类筛选
|
||||
- 功能优化:允许用户在聊天页面设置是否使用流式输出或者一次性输出,兼容 GPT-O1 模型。
|
||||
- 功能优化:移除 PayJS 支付渠道支持,PayJs 已经关闭注册服务,请使用其他支付方式。
|
||||
- 功能新增:新增 GeeK 易支付支付渠道,支持支付宝,微信支付,QQ 钱包,京东支付,抖音支付,Paypal 支付等支付方式
|
||||
- Bug 修复:修复注册页面 tab 组件没有自动选中问题 [#6](https://github.com/yangjian102621/geekai-plus/issues/6)
|
||||
- 功能优化:Luma 生成视频任务增加自动翻译功能
|
||||
- Bug 修复:Suno 和 Luma 任务没有判断用户算力
|
||||
- 功能新增:邮箱注册增加邮箱后缀白名单,防止使用某些垃圾邮箱注册薅羊毛
|
||||
- 功能优化:清空未支付订单时,只清空超过 15 分钟未支付的订单
|
||||
|
||||
## v4.1.3
|
||||
|
||||
- 功能优化:重构用户登录模块,给所有的登录组件增加行为验证码功能,支持用户绑定手机,邮箱和微信
|
||||
- 功能优化:重构找回密码模块,支持通过手机或者邮箱找回密码
|
||||
- 功能优化:管理后台给可以拖动排序的组件添加拖动图标
|
||||
- 功能优化:Suno 支持合成完整歌曲,和上传自己的音乐作品进行二次创作
|
||||
- Bug 修复:手机端角色和模型选择不生效
|
||||
- Bug 修复:用户登录过期之后聊天页面出现大量报错,需要刷新页面才能正常
|
||||
- 功能优化:优化聊天页面 Websocket 断线重连代码,提高用户体验
|
||||
- 功能优化:给算力增减服务全部加上数据库事务和同步锁
|
||||
- 功能优化:支持用户在前端对话界面选择插件
|
||||
- 功能新增:支持 Luma 文生视频功能
|
||||
|
||||
## v4.1.2
|
||||
|
||||
- Bug 修复:修复思维导图页面获取模型失败的问题
|
||||
- 功能优化:优化 MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力
|
||||
- Bug 修复:修复后台拖动排序组件 Bug
|
||||
- 功能优化:更新数据库失败时候显示具体的的报错信息
|
||||
- Bug 修复:修复管理后台对话详情页内容显示异常问题
|
||||
- 功能优化:管理后台新增清空所有未支付订单的功能
|
||||
- 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求
|
||||
- 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力
|
||||
|
||||
## v4.1.1
|
||||
|
||||
- Bug 修复:修复 GPT 模型 function call 调用后没有输出的问题
|
||||
- 功能新增:允许获取 License 授权用户可以自定义版权信息
|
||||
- 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件
|
||||
- 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
|
||||
- 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
|
||||
- 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
|
||||
- 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式
|
||||
- 功能新增:允许在管理后台设置首页显示的导航菜单
|
||||
- Bug 修复:修复注册页面先显示关闭注册组件,然后再显示注册组件
|
||||
- 功能新增:增加 Suno 文生歌曲功能
|
||||
- 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加
|
||||
- 功能优化:在所有列表页面增加返回顶部按钮
|
||||
|
||||
## v4.1.0
|
||||
|
||||
- bug 修复:修复移动端修改聊天标题不生效的问题
|
||||
- Bug 修复:修复用户注册不显示用户名的问题
|
||||
- Bug 修复:修复管理后台拖动排序不生效的问题
|
||||
- 功能优化:允许用户设置自定义首页背景图片
|
||||
- 功能新增:**支持 AI 解读 PDF, Word, Excel 等文件**
|
||||
- 功能优化:优化聊天界面的用户上传文件的列表样式
|
||||
- 功能优化:优化聊天页面对话样式,支持列表样式和对话样式切换
|
||||
- 功能新增:支持微信扫码登录,未注册用户微信扫码后会自动注册并登录。移动使用微信浏览器打开可以实现无感登录。
|
||||
|
||||
## v4.0.9
|
||||
|
||||
- 环境升级:升级 Golang 到 go1.22.4
|
||||
- 功能增加:接入微信商户号支付渠道
|
||||
- Bug 修复:修复前端页面菜单把页面撑开,底部留白问题
|
||||
- 功能优化:聊天页面自动根据内容调整输入框的高度
|
||||
- Bug 修复:修复 Dalle 绘图失败退回算力的问题
|
||||
- 功能优化:邀请码注册时被邀请人也可以获得赠送的算力
|
||||
- 功能优化:允许设置邮件验证码的抬头
|
||||
- Bug 修复:修复免费模型不会记录聊天记录的 bug
|
||||
- Bug 修复:修复聊天输入公式显示异常的 Bug
|
||||
|
||||
## v4.0.8
|
||||
|
||||
- 功能优化:升级 mathjax 公式解析插件,修复公式因为图片访问限制而无法显示的问题
|
||||
- 功能优化:当数据库更新失败的时候记录错误日志
|
||||
- 功能优化:聊天输入框会随着输入内容的增多自动调整高度
|
||||
- Bug 修复:修复移动端聊天页面模型切换不生效的 Bug
|
||||
- 功能优化:给 PC 端扫码支付增加签名验证和有效期验证
|
||||
- Bug 修复:修复支付码生成 API 权限控制的问题
|
||||
- Bug 修复:模型算力设置为 0 时,不扣减用户算力,并且不记录算力消费日志
|
||||
- 功能优化:新增随机背景配置项,可以在后台设置,首页使用 Bing 壁纸作为背景图片
|
||||
- 功能新增:H5 端支持 Dalle 绘图
|
||||
|
||||
## v4.0.7
|
||||
|
||||
- 功能优化:添加导航菜单的时候支持框入外部链接,并支持上传自定义菜单图片
|
||||
- Bug 修复:修复弹窗等于图形验证码一直验证失败的问题
|
||||
- 功能重构:重构前端 UI 页面,增加顶部导航
|
||||
- 功能优化:优化 Vue 非父子组件之间的通信方式
|
||||
- 功能优化:优化 ItemList 组件,自动根据页面宽度计算 cols 数量
|
||||
|
||||
## v4.0.6
|
||||
|
||||
- Bug 修复:修复 PC 端画廊页面的瀑布流组件样式错乱问题
|
||||
- 功能新增:给思维导图增加 ToolBar,实现思维导图的放大缩小和定位
|
||||
- Bug 修复:修复思维导图不扣费的 Bug
|
||||
- Bug 修复:修复管理后台角色删除失败的 Bug
|
||||
- Bug 修复:兼容最新版秋叶 SD 懒人包的 SD API,新增 scheduler 参数
|
||||
- 功能优化:支持在管理后台配置 AI 绘图相关配置,包括 SD, MJ-PLUS, MJ-PROXY
|
||||
- Bug 修复:修复注册用户提示注册人数达到上限的 Bug
|
||||
- 功能优化:将 MJ,SD,Dall 绘画页面的任务列表全改成瀑布流组件
|
||||
|
||||
## v4.0.5
|
||||
|
||||
- 功能优化:已授权系统在后台显示授权信息
|
||||
- 功能优化:使用思维链提示词生成思维导图,确保生成的思维导图不会出现格式错误
|
||||
- 功能优化:优化首页登录注册页面的 UI
|
||||
- BUG 修复:修复 License 验证的逻辑漏洞
|
||||
- Bug 修复:后台添加用户的时候密码规则限制跟前台注册保持一致
|
||||
- 功能新增:管理后台支持切换主题,支持 light 和 dark 两种主题
|
||||
- 功能新增:移动端新增 DALL-E 绘画功能
|
||||
- 功能新增:新增移动端首页功能,移动端支持 light 和 dark 两种主题
|
||||
- 功能新增:移动支持免登录预览功能
|
||||
- Bug 修复:解决在同一个浏览器开启多个对话时候对话内容会相互乱串的问题
|
||||
- Bug 修复:修复部分中转 API 模型会出现第一输出的字符被淹没的 Bug
|
||||
|
||||
## v4.0.4
|
||||
|
||||
- Bug 修复:修复统一千问第二句不回复的问题
|
||||
- 功能优化:MJ 和 SD 任务正在执行时不更新已完成任务列表,加快页面渲染速度
|
||||
- 功能新增:Dalle AI 绘画功能实现
|
||||
- Bug 修复:修复思维导图格式乱码问题
|
||||
- 功能优化:支持使用 TLS 邮件协议,解决国内服务器无法使用 25 号端口发送邮件的问题
|
||||
- 功能新增:支持从应用列表直接和某个应用对话
|
||||
- 功能优化:优化算力日志的页面和首页的 UI
|
||||
- 功能新增:支持思维导图导出 PNG 图片下载
|
||||
|
||||
## v4.0.3
|
||||
|
||||
- 功能新增:允许为角色应用绑定模型,如指定某个角色只能使用某个模型
|
||||
- Bug 修复:兼容 gpt-4-turbo-2024-04-09 模型的函数调用 Bug
|
||||
- Bug 修复:修复 MidJourney 在任务超时后出现后面的任务覆盖前面任务的问题
|
||||
- 功能新增:支持上传图片和视觉模型
|
||||
- 功能优化:优化聊天页面的复制代码按钮样式乱码
|
||||
- 功能新增:增加思维导图功能,支持选择不同的对话模型来生成思维导图
|
||||
- 功能新增:支持为角色绑定对话模型,比如绑定某个角色只能用 GPT3.5 或者 GPT4
|
||||
- 功能新增:支持为模型绑定 API KEY,比如为 GPT3.5 模型绑定免费的 API KEY 给用户免费使用来引流不至于消耗你的收费 KEY。
|
||||
- 功能新增:支持管理后台 Logo 修改
|
||||
|
||||
## 4.0.2
|
||||
|
||||
- 功能新增:支持前端菜单可以配置
|
||||
- 功能优化:在登录和注册界面标题显示软件版本号
|
||||
- 功能优化:MJ 绘画支持 --sref 和 --cref 图片一致性参数
|
||||
- 功能优化:使用 leveldb 解决 SD 绘图进度图片预览问题
|
||||
- Bug 修复:解决因为图片上传使用相对路径而导致融图失败的问题。
|
||||
- 功能新增:手机端支持 Stable-Diffusion 绘画
|
||||
- 功能新增:管理后台登录页面增加行为验证码,防止爆破
|
||||
|
||||
## v4.0.1
|
||||
|
||||
- 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口,SDAPI 兼容各种 stable-diffusion
|
||||
发行版,稳定性更强一些
|
||||
- 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API,兼容
|
||||
MJ-Plus 中转
|
||||
- 功能新增:用户算力消费日志增加统计功能,统计一段时间内用户消费的算力
|
||||
- Bug 修复:修复 iphone 手机无法通过图形验证码的 Bug,使用滑动验证码替换
|
||||
- Bug 修复:修复手机端 MidJourney 绘画页面滚动条无法滚动的 Bug
|
||||
|
||||
## v4.0.0
|
||||
|
||||
非兼容版本,重大重构,引入算力概念,将系统中所有的能力(AI 对话,MJ 绘画,SD 绘画,DALL 绘画)全部使用算力来兑换。
|
||||
只要你的算力值余额不为 0,你就可以进行任何操作。比如一次 GPT3.5 对话消耗 1 个单位算力,一次 GPT4 对话消耗 10 个算力。一次 MJ
|
||||
对话消耗 15 个算力...
|
||||
|
||||
- 功能重构:重构整体系统,全部采用算力来进行结算
|
||||
- 功能优化:SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽
|
||||
- 功能优化:移动端聊天页面图片支持预览和放大功能
|
||||
- 功能优化:MJ 和 SD 页面数据分页加载,解决一次性加载太多数据导致页面卡顿的问题
|
||||
- 功能优化:**PC 端不登录也可以预览功能,只有在发起操作的时候才需要登录**
|
||||
- 功能优化:控制台订单管理页面显示未支付订单,并提供订单删除功能
|
||||
- 功能新增:支持 H5 支付
|
||||
- 功能优化:支持数学公式的识别和美化输出
|
||||
- 功能新增:新增算力消费日志功能
|
||||
- 功能优化:整合 XXL-JOB 实现订单清理,每日算力派发,VIP 算力重置等任务
|
||||
- 功能新增:管理后台新增 7 日内新增用户和新增订单统计
|
||||
|
||||
## v3.2.7
|
||||
|
||||
- 功能重构:采用 Vant 重构移动页面,新增 MidJourney 功能
|
||||
- 功能优化:优化 PC 端 MidJourney 页面布局,新增融图和换脸功能
|
||||
- Bug 修复:修复 issue [
|
||||
管理界面操作用户存在的两个问题](https://github.com/yangjian102621/chatgpt-plus/issues/117#issuecomment-1909201532)
|
||||
- 功能优化:在对话和聊天记录表中新增冗余字段 model,存储对话模型
|
||||
- Bug 修复:IPhone 手机验证码触摸事件坐标错位 [issue 144](https://github.com/yangjian102621/chatgpt-plus/issues/144)
|
||||
- Bug 修复:重新生成按钮功能失效问题
|
||||
- Bug 修复:对话输入 HTML 标签不显示的问题
|
||||
- 功能优化:gpt-4-all/gpts/midjourney-plus 支持第三方平台的 API KEY
|
||||
- 功能新增:新增删除文件功能
|
||||
- Bug 修复:解决 MJ-Plus discord 图片下载失败问题,使用第三方平台中转地址下载
|
||||
- 功能新增:后台管理新怎对话查看和检索功能
|
||||
|
||||
## v3.2.6
|
||||
|
||||
- 功能优化:恢复关闭注册系统配置项,管理员可以在后台关闭用户注册,只允许内部添加账号
|
||||
- 功能优化:兼用旧版本微信收款消息解析
|
||||
- 功能优化:优化订单扫码支付状态轮询功能,当关闭二维码时取消轮询,节约网络资源
|
||||
- 功能新增:新增图片发布功能,画廊只显示用户已发布的图片
|
||||
- 功能新增:后台新增配置微信客服二维码,可以上传自己的微信客服二维码
|
||||
- 功能新增:新增网站公告,可以在管理后台自定义配置
|
||||
- 功能新增:新增阿里通义千问大模型支持
|
||||
- Bug 修复:修复 MJ 放大任务失败时候 img_call 会增加的 Bug
|
||||
- 功能优化:新增虎皮椒和 PayJS 订单状态校验功能,增加安全性
|
||||
- Bug 修复:修复微信转账交易 ID 提取失败 Bug
|
||||
- 功能优化:给所有的 websocket 连接加上心跳,解决 "close 1006 (abnormal closure): unexpected EOF" Bug
|
||||
- 功能新增:新增短信宝短信平台发送平台集成
|
||||
|
||||
## v3.2.5
|
||||
|
||||
- 功能新增:**重磅更新!!!** 新增 MidJourney-Plus API 支持,一秒配置,开箱即用,高效稳定。
|
||||
- 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅
|
||||
Plus 账号了!!!
|
||||
- 功能优化:增强 markdown 图片和引用块解析。
|
||||
- 功能新增:新增用户文件管理,目前一支持上传文件跟 GPT 进行多态对话。
|
||||
- 功能优化:function call 兼用中转 API。
|
||||
- Bug 修复:修复部分已知的 Bug。
|
||||
|
||||
## v3.2.4.1
|
||||
|
||||
- 功能新增:新增 PayJs 支付通道
|
||||
- Bug 修复:紧急修复后台添加用户失败问题
|
||||
- Bug 修复:紧急修复使用中转 API-KEY 无法绘图的问题
|
||||
- Bug 修复:允许用户关闭手机和邮箱注册通道,移除验证码依赖
|
||||
|
||||
## v3.2.4
|
||||
|
||||
- 功能新增:重磅更新,支持邮箱注册
|
||||
- 功能优化:优化函数调用授权
|
||||
- 功能优化:给用户表新增 nickname 字段
|
||||
- 功能优化:管理后台给聊天角色增加启用/禁用开关
|
||||
- Bug 修复:SD 绘画出现重复扣减绘图次数
|
||||
- 功能优化:优化聊天对话导出样式,适应移动端
|
||||
- 功能新增:众筹核销可以选择兑换对话还是绘图的额度
|
||||
- Bug 修复:修复[从历史记录获取 reply 有并发风险 #92](https://github.com/yangjian102621/chatgpt-plus/issues/92)
|
||||
- Bug 修复:修复 MidJourney 绘图任务调度 Bug,为 task_id 建议唯一索引
|
||||
- 功能重构:重构了 API KEY 模块,支持为每个 API KEY 都设置不同的 API 地址,并可以单独开启是否使用代理。
|
||||
|
||||
## v3.2.3
|
||||
|
||||
- 功能重构:重构函数工具模块,设计成可以后台动态管理函数。支持添加自定义函数实现
|
||||
- 功能新增:为充值产品数据表添加 img_calls 字段,支持充值绘图次数
|
||||
- Bug 修复:修复 [MJ 机器人空指针异常的 Bug](https://github.com/yangjian102621/chatgpt-plus/issues/73)
|
||||
- Bug 修复:确保相同 Prompt 的绘图任务的 Upscale 和 Variation 任务调度给相同的频道
|
||||
- 功能新增:新增删除绘图任何和图片功能
|
||||
- Bug 修复:修复虎皮椒支付二维码重复扫码时报错问题
|
||||
- 功能优化:自动将 AI 绘画中的中文提示词翻译成英文
|
||||
- 功能优化:优化 AI 绘画的大图压缩算法,新增图片缓存
|
||||
- 功能优化:支持为 MJ 绘图 API 增加反代功能,提高图片的加载速度,大大降低绘图任务的失败率
|
||||
- Bug 修复:修复[Azure Api 更换 api-version 参数后请求失败的问题](https://github.com/yangjian102621/chatgpt-plus/pull/71)
|
||||
- Bug 修复:修复科大讯飞 V1.5 API 请求失败的问题
|
||||
- Bug 修复:绘图失败后,自动恢复用户的剩余绘图次数
|
||||
- 功能新增:为移动端新增 SD 绘图功能,分享功能
|
||||
|
||||
## v3.2.2
|
||||
|
||||
- 功能重构:重构 MidJourney 和 Stable-Diffusion 绘图模块,支持使用多组配置创建池子提供绘画服务
|
||||
- 功能新增:AI 绘画页面增加翻译和重写提示词功能
|
||||
- 功能优化:OSS 上传组件支持在 Bucket 下设置二级目录
|
||||
- Bug 修复:修复阿里云 OSS 访问路径错误
|
||||
- 功能优化:在 AI 绘图页面使用 HTTP 轮询替换 Websocket
|
||||
|
||||
## v3.2.1
|
||||
|
||||
- 功能优化:切换角色和模型的时候自动创建新的对话
|
||||
- Bug 修复:修复文件上传失败 No such file bug
|
||||
- 功能新增:MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数
|
||||
- Bug 修复:[PC 端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59)
|
||||
- 功能新增:增加 arm64 架构打包脚本
|
||||
- 功能新增:支持 dall-e3 绘图的 API 地址自定义配置
|
||||
- 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道
|
||||
|
||||
## v3.2.0
|
||||
|
||||
- 功能新增:新增邀请注册功能
|
||||
- 功能优化:增加中间件自动对 HTTP 请求的参数去掉首尾空格
|
||||
- 功能优化:增加中间件自动为大图片生成缩略图
|
||||
- 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载
|
||||
- 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
|
||||
- Bug 修复:修复阿里云 OSS 域名设置不起做用的 bug
|
||||
- Bug 修复:修复 MidJourney 绘图失败后重复添加到队列的问题
|
||||
|
||||
## v3.1.9
|
||||
|
||||
- 功能新增:增加讯飞星火大模型 v3.0 支持
|
||||
- 功能新增:新增找回密码功能
|
||||
- 功能新增:支持 Markdown 代码复制功能
|
||||
- Bug 修复: xxl-job 任务调度失败的 Bug
|
||||
- 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font
|
||||
- Bug 修复:Stable-Diffusion 绘画成功之后没有扣减用户画图次数
|
||||
- 功能优化:优化会员充值页面 ItemList 组件
|
||||
- 功能优化:给首页 Logo 增加链接
|
||||
- Bug 修复:[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51)
|
||||
- Bug 修复:聊天上下文失效问题
|
||||
- 功能优化:关闭注册时显示联系管理员二维码
|
||||
- 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能
|
||||
- Bug 修复:后台启用用户 VIP 不生效问题
|
||||
- 功能优化:充值支付页面的支付说明文字可以后台配置
|
||||
- Bug 修复:ChatGLM,百度文心,科大讯飞模型输出代码不换行问题
|
||||
|
||||
## v3.1.8
|
||||
|
||||
1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
|
||||
2. Bug 修复:修复 MidJourney API 参数版本更新导致调用失败问题
|
||||
3. Bug 修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题
|
||||
4. Bug 修复:修复七牛云上传报错 expired token
|
||||
5. Bug 修复:修复高权重模型导致的对话次数为负数的漏洞
|
||||
6. 功能优化:将聊天报错信息定义为统一常量,方便修改
|
||||
7. 功能优化:优化 markdown 表格显示样式,覆写 Element-Plus 表格样式
|
||||
8. 功能优化:增加倒数计时组件,定期自动清理未支付的订单
|
||||
|
||||
## v3.1.7
|
||||
|
||||
1. 功能新增:支持文心 4.0 AI 模型
|
||||
2. 功能新增:可以在管理后台为用户绑定指定的 AI 模型,如只给某个用户使用 GPT-4 模型
|
||||
3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如 GPT4 是 GPT3.5 的 10 倍
|
||||
4. 功能新增:新增系统配置关闭 AI 模型的函数功能
|
||||
5. 功能优化:优化 MidJourney 专业绘画页面图片预览样式
|
||||
|
||||
## v3.1.6
|
||||
|
||||
1. 功能新增:新增 AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品
|
||||
2. 功能新增:新增 AI 角色应用功能页面,用户可以添加自己感兴趣的应用
|
||||
3. 功能优化:优化瀑布流组件的页面布局
|
||||
4. 功能优化:新注册用户成功之后自动登录
|
||||
5. 功能优化:优化更新对话标题的操作体验,绑定回车事件
|
||||
|
||||
## v3.1.5
|
||||
|
||||
1. 功能新增:新增百度文心一言大模型 API 接入支持
|
||||
2. 功能新增:新增科大讯飞星火大模型 API 接入支持
|
||||
3. 功能重构:将 chat_handler 的所有功能实现放入单独的包中
|
||||
4. 功能新增:新增系统配置 `enabled_function` 用于启用和关闭函数功能
|
||||
5. Bug 修复:修复管理后台更新 API Key 失败的 Bug
|
||||
6. Bug 修复:修复新建的对话无法更新对话标题的 Bug
|
||||
7. 功能优化:其他一些小的体验优化工作
|
||||
|
||||
## v3.1.4
|
||||
|
||||
1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio 和阿里云 OSS 四种存储介质。
|
||||
2. 功能新增:**增加 Stable Diffusion 绘画功能页面**。
|
||||
3. 功能重构:将 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts) 合并到本项目,部署更加简单,无需部署两个项目了。
|
||||
4. Bug 修复:修复[用户注册报错 BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。
|
||||
5. Bug 修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。
|
||||
6. 功能优化:增加阿里云短信服务配置项 `Sign` 和 `CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。
|
||||
7. 功能优化:添加系统配置用来设置自定义的众筹微信收款二维码。
|
||||
8. 功能优化:优化绘画页面的弹窗样式和页面布局。
|
||||
|
||||
## v3.1.3
|
||||
|
||||
1. 页面重构:重后 Home 页面,拆分成聊天,MJ 绘画,SD 绘画,应用广场等多个功能菜单。
|
||||
2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。
|
||||
3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。
|
||||
4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。
|
||||
5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。
|
||||
6. Bug 修复:修复 JWT token 有效期计算错误的 Bug。
|
||||
|
||||
## v3.1.2
|
||||
|
||||
1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。
|
||||
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
|
||||
3. Bug 修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
|
||||
4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。
|
||||
5. 功能优化:取消导出聊天页面的授权验证
|
||||
6. 功能优化:所有路由跳转都使用绝对路径
|
||||
|
||||
## v3.1.1
|
||||
|
||||
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的 Bug
|
||||
|
||||
## v3.1.0(大版本更新)
|
||||
|
||||
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及
|
||||
ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
|
||||
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
|
||||
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储 handler,目前已经实现本地存储和 minio oss 存储。
|
||||
4. 功能优化:更新头像自动删除旧的图片资源。
|
||||
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
|
||||
6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。
|
||||
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
|
||||
8. 升级 gin, element-plus,redis 组件到最新版本。
|
||||
9. Bug 修复:修复若干已知的的 Bug
|
||||
|
||||
## v3.0.7
|
||||
|
||||
1. 聊天主界面:新增聊天引导页面,介绍产品功能
|
||||
2. 功能重构:拆分项目,将函数插件以及微信机器人,MidJourney 机器人等功能拆分新项目独立部署。
|
||||
3. 功能新增:新增 MidJourney AI 绘画支持,当识别到用户的绘画需求时,自动调用 MidJourney 绘画函数进行绘画。
|
||||
4. 功能新增:支持导出聊天记录为 PDF 文件。
|
||||
5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。
|
||||
6. 功能优化:支持用户设置默认的 GPT 模型
|
||||
7. Bug 修复:修复若干已知的的 Bug
|
||||
|
||||
## v3.0.6
|
||||
|
||||
1. 管理后台:新增用户名和手机号码搜索功能
|
||||
2. 管理后台:新增重置用户密码功能
|
||||
3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景
|
||||
4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗
|
||||
5. Bug 修复:修复注册页面验证码不显示 Bug
|
||||
6. Bug 修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
|
||||
7. 功能修正:允许用户使用手机号码登录
|
||||
8. 功能优化:更新系统配置后同步更新服务端内存变量数据
|
||||
9. 功能优化:优化打包脚本,减少容器镜像大小
|
||||
|
||||
## v3.0.5
|
||||
|
||||
重磅功能更新!!! 新增函数插件支持,可以轻松地接入你的第三方插件服务,ChatGPT 自动帮您调用对应的函数完成任务。
|
||||
|
||||
1. 新增函数功能支持,全球早报,今日头条和微博热搜等插件服务,您也可以接入自己的第三方服务。
|
||||
2. 集成微信机器人模块,可以通过微信个人收款码来完成充值,无需接入微信支付功能也可以完成收款功能。
|
||||
3. 用户注册添加短信验证码功能,引入交互安全认证服务,有效防刷短信。
|
||||
4. 支持配置聊天上下文深度,精确统计每轮对话所消耗的总 TOKEN 数量。
|
||||
5. 修复已知的 Bug。
|
||||
|
||||
## v3.0.4
|
||||
|
||||
1. 调整项目目录结构,移除其他语言 API 目录
|
||||
2. 修复 nodejs apple M1 跨平台打包,运行报错 exec format error
|
||||
3. 增加用户 token 消耗统计功能
|
||||
|
||||
## v3.0.3
|
||||
|
||||
1. 优化启动参数接收处理,支持环境变量传参
|
||||
2. 修复 PC 端聊天界面出现滚动条的 Bug
|
||||
3. 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题
|
||||
4. 更改 docker 构建镜像的基础镜像,改用作者的阿里云镜像,这样打包更快一些。
|
||||
|
||||
## v3.0.2
|
||||
|
||||
1. Feat:新增移动端的聊天和用户设置功能
|
||||
2. Fix: 修复 markdown 换行符解析的 Bug
|
||||
3. Feat: 新增头像上传功能
|
||||
4. Docs: 增加容器部署支持,支持 docker-compose 一键部署
|
||||
5. Fix: 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug
|
||||
|
||||
## v3.0.1
|
||||
|
||||
1. 紧急修复前端 Home 组件路由被后台管理 Home 组件路由覆盖的 Bug。
|
||||
2. 增加 docker-compose 部署脚本
|
||||
|
||||
## v3.0.0
|
||||
|
||||
全新的重构版本!!!
|
||||
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
|
||||
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。
|
||||
前后台技术架构还是基于 `Vue3 + Element-Plus` ,但是页面风格已经全部变了,几乎所有页面样式代码都重写了,希望会你是希望的风格!
|
||||
|
||||
此次重构改版主要是为了后面功能的扩展准备了。
|
||||
|
||||
新版本已经实现的功能如下:
|
||||
|
||||
1. 引入用户体系,新增用户注册和登录功能。
|
||||
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
|
||||
3. 创建会话的时候可以选择聊天角色和模型。
|
||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
|
||||
5. 保存聊天记录,支持聊天上下文。
|
||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
|
||||
7. 引入 ip2region 组件,记录用户的登录 IP 和地址。
|
||||
8. 支持会话搜索过滤。
|
||||
@@ -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
|
||||
@@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2023 RockYang
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# 项目固定指令(Cursor 必须遵守)
|
||||
|
||||
1. 代码必须使用清晰命名,禁止单字母变量
|
||||
2. 代码必须加注释
|
||||
3. 统一使用 4 空格缩进
|
||||
4. 前端样式代码永远优先适应 Tailwind CSS 的样式, 别自定义样式。除非 Tailwind CSS 不支持或者做不到。
|
||||
5. 不要生成多余代码,保持简洁
|
||||
6. 每次生成代码前遵守以上规则
|
||||
@@ -1,272 +1,77 @@
|
||||
# ChatGPT-Plus
|
||||
# 🚀 GeekAI:一站式 AI 创意生产力平台
|
||||
|
||||
基于 OpenAI API 实现的 ChatGPT Web 应用,一共分为两个版本:
|
||||
**让创意触手可及,让创作变得简单**
|
||||
|
||||
* 通用版:交互体验跟 ChatGPT 官方一致,聊天记录保存在客户端(浏览器)
|
||||
* 角色版:内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||
聊天记录保存在云端(可以配置是否保存聊天记录)
|
||||
每个版本都有 PC 版和移动版,PC 版本的体验要略优于移动版。
|
||||
一个功能完整、开箱即用的 AI 多模态内容创作平台,集成了对话、绘画、音乐、视频、思维导图等全链路 AI 创作能力。无论是个人创作者还是企业团队,都能快速搭建属于自己的 AI 创作工作台。
|
||||
|
||||
**本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用,优秀项目我们将在项目首页为您展示。**
|
||||
## ✨ 核心特色
|
||||
|
||||
### 🎨 **全能 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://chat.geekai.me)
|
||||
|
||||
## 文档地址
|
||||
|
||||
[Geek-AI 文档](https://docs.geekai.me)
|
||||
|
||||
## 部署
|
||||
|
||||
1. 安装 docker 和 docker-compose 程序,这个自行解决。
|
||||
2. 直接在项目根目录运行启动命令:
|
||||
```shell
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 功能截图
|
||||
|
||||
### 1. 角色版PC端
|
||||

|
||||
|
||||

|
||||
|
||||
### 2. 角色版移动端
|
||||

|
||||
|
||||
### 3. 通用版
|
||||

|
||||
|
||||
### 4. 管理后台
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 5. 体验地址
|
||||
> 体验地址:[https://www.chat-plus.net/chat](https://www.chat-plus.net/chat) <br/>
|
||||
> 涉及到数据隐私问题,没有提供共享账号,大家自己快速注册一个账号就可以免费体验
|
||||
|
||||
## 项目介绍
|
||||
这一套完整的系统,包括两套前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供 ChatGPT 的服务。
|
||||
|
||||
项目的技术架构是
|
||||
|
||||
> Go + Vue3 + element-plus
|
||||
|
||||
后端采用的是 Go 语言开发的 Gin Web 框架。前端用的是 Vue3 + element-plus UI 框架
|
||||
|
||||
目前已经实现了以下功能:
|
||||
1. 通用版的 ChatGPT 聊天界面和功能,聊天记录保存在客户端。
|
||||
2. 口令机制:输入口令才可以访问,支持设置口令的对话次数,有效期。
|
||||
3. 角色版的聊天界面和功能,角色设定,预设一些角色,比如程序员,客服,作家,老师,艺术家...
|
||||
4. 保存聊天记录,支持聊天上下文。
|
||||
5. OpenAI API 负载均衡,限制每个 API Key 每分钟之内调用次数不超过 15次,防止被封。
|
||||
6. 支持配置多个代理,保证高可用。
|
||||
7. 实现 markdown 语法解析和代码高亮,支持复制回复内容功能。
|
||||
8. 后台管理功能,实现系统的动态配置,口令和角色的动态管理。
|
||||
|
||||
## 项目地址
|
||||
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
|
||||
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
|
||||
|
||||
## TODOLIST
|
||||
* [ ] 让用户配置自己的 API KEY,调用自己的 API Key,将不消耗口令的对话次数
|
||||
* [ ] 嵌入 AI 绘画功能,支持根据描述词生成图片
|
||||
* [ ] 接入自己训练的开源大语言模型
|
||||
* [ ] 接入 Google 语音 API,支持语音聊天
|
||||
|
||||
## 快速本地部署
|
||||
如果你想在本地快速尝鲜一下,你可以直接下载我打包好的一键运行包,然后直接运行就好了,就这么简单。
|
||||
支持 windows, linux 和 Mac 系统,[GitHub 下载地址](https://github.com/yangjian102621/chatgpt-plus/releases)。
|
||||
|
||||
我这里以 linux 系统为例,演示一下部署过程:
|
||||
|
||||
```shell
|
||||
# 1. 下载程序
|
||||
wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v2.0.0/wechatGPT-amd64-linux
|
||||
# 2. 添加执行权限
|
||||
chmod +x wechatGPT-amd64-linux
|
||||
# 3. 运行程序
|
||||
./wechatGPT-amd64-linux
|
||||
```
|
||||
服务启动成功之后直接访问后台管理页面(http://localhost:5678/chat/#/admin),初始用户名和密码分别为 `admin` 和 `admin123`。
|
||||
|
||||
修改系统配置。这里主要配置 OpenAI 的 [API KEY](https://platform.openai.com/account/api-keys) 和魔法,因为调用 OpenAI 的 API 需要魔法。
|
||||
|
||||

|
||||
|
||||
最后,你还需要一个口令来访问 ChatGPT 前端应用。直接在【后台管理系统->口令管理】新增一个口令就好了。
|
||||
|
||||

|
||||
|
||||
**切记要先退出后台登录,再登录前端应用!!!前后端账号权限不一致。**
|
||||
|
||||
然后直接使用口令名称登录前端应用:
|
||||
|
||||
* 角色版:http://localhost:5678/chat/#/plus
|
||||
* 普通版:http://localhost:5678/chat/#/free
|
||||
|
||||
祝你使用愉快!
|
||||
|
||||
## 本地开发调试
|
||||
本项目的技术架构是
|
||||
|
||||
> Go + Vue3 + element-plus
|
||||
|
||||
所以你需要提前安装好 Go(1.19以上)语言环境和 Nodejs 环境。
|
||||
|
||||
### 1. 运行后端程序
|
||||
|
||||
首先,我们需要拷贝默认的配置文档:
|
||||
```shell
|
||||
cd src
|
||||
cp config.sample.toml config.toml
|
||||
```
|
||||
然后,我们需要修改配置文档,需要修改的有三处,一个是魔法,第二个是 API KEY,第三个是修改 Session 跨域的域名。因为我们前后端是分离的,所以必须解决前端访问跨域的问题。
|
||||
|
||||
```toml
|
||||
ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理,如:http://127.0.0.1:7777
|
||||
|
||||
[Session]
|
||||
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80"
|
||||
Name = "CHAT_SESSION_ID"
|
||||
Path = "/"
|
||||
Domain = "172.22.11.200" # 这里需要改成你当前机器的内网 IP 地址
|
||||
MaxAge = 86400
|
||||
Secure = false
|
||||
HttpOnly = false
|
||||
SameSite = 2
|
||||
|
||||
[[Chat.ApiKeys]] # API KEY 可以添加多个,自动负载均衡
|
||||
Value = "YOUR_OPENAI_API_KEY" # 这里替换成你的 OpenAI API KEY: sk-xxxx
|
||||
LastUsed = 0
|
||||
```
|
||||
|
||||
修改之后运行后端程序:
|
||||
|
||||
```shell
|
||||
# 1. 先下载依赖
|
||||
go mod tidy
|
||||
# 2. 运行程序
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### 2. 运行前端程序
|
||||
同样先拷贝配置文档:
|
||||
```shell
|
||||
cd web
|
||||
cp .env.production .env.development
|
||||
```
|
||||
编辑 `.env.development` 文件,修改后端 API 的访问路径:
|
||||
|
||||
```ini
|
||||
VUE_APP_API_HOST=http://172.22.11.200:5678 # 这里也是需要把 IP 替换成你本地的内网 IP 地址
|
||||
VUE_APP_WS_HOST=ws://172.22.11.200:5678
|
||||
```
|
||||
|
||||
配置好了之后就可以运行前端应用了:
|
||||
```
|
||||
# 安装依赖
|
||||
npm install
|
||||
# 运行
|
||||
npm run dev
|
||||
```
|
||||
|
||||
启动之后通过**内网地址**访问后台管理页面:http://172.22.11.200:8888/chat/#/admin ,添加一个口令就可以开始测试了。
|
||||
|
||||
## 项目打包
|
||||
由于本项目是采用异构开发的方式,所项目打包分成两步:首先打包前端应用,然后再将打包后前端文件打包进后端应用,最后打成一个包。
|
||||
|
||||
### 打包前端
|
||||
首先你还是需要修改配置文档 `.env.production`,指定你线上部署的域名。
|
||||
|
||||
```ini
|
||||
VUE_APP_API_HOST=https://www.chat-plus.net # API 请求主机
|
||||
VUE_APP_WS_HOST=wss://www.chat-plus.net # websocket 请求主机
|
||||
```
|
||||
|
||||
执行打包
|
||||
```shell
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 打包后端
|
||||
你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。
|
||||
```shell
|
||||
# for all platforms
|
||||
make all
|
||||
# for linux only
|
||||
make linux
|
||||
```
|
||||
打包后的可执行文件在 `src/bin` 目录下。
|
||||
|
||||
### 线上部署
|
||||
部署方式跟 [快速本地部署](#快速本地部署) 一样,将打包好的可执行文件在线上服务器运行即可。
|
||||
|
||||
> **特别注意:** 线上发布请记得修改配置文档中的 AccessKey, 以免给你的应用造成风险!!!
|
||||
|
||||
**另外,如果你部署在国外的服务器,那么就不需要配置代理,直接留空就行了。**
|
||||
|
||||
```shell
|
||||
ProxyURL = []
|
||||
```
|
||||
### 使用 Nginx 代理
|
||||
|
||||
```nginx
|
||||
# 这里需要配置允许 Websocket 请求转发,非常重要
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'websocket' upgrade;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name www.chat-plus.net;
|
||||
|
||||
ssl_certificate xxx.pem; # SSL 证书
|
||||
ssl_certificate_key xxx.key;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
underscores_in_headers on;
|
||||
access_log /var/log/chatgpt/access.log;
|
||||
error_log /var/log/chatgpt/error.log;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
|
||||
|
||||
}
|
||||
|
||||
# 关闭静态资源日志
|
||||
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css)$ {
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 参与贡献
|
||||
|
||||
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
|
||||
|
||||
如果有兴趣的话,也可以加微信进入微信讨论群。
|
||||
|
||||

|
||||
|
||||
#### 特此声明:不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
|
||||
|
||||
### Commit 类型
|
||||
* feat: 新特性或功能
|
||||
* fix: 缺陷修复
|
||||
* docs: 文档更新
|
||||
* style: 代码风格或者组件样式更新
|
||||
* refactor: 代码重构,不引入新功能和缺陷修复
|
||||
* opt: 性能优化
|
||||
* chore: 一些不涉及到功能变动的小提交,比如修改文字表述,修改注释等
|
||||
|
||||
|
||||
## 打赏
|
||||
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
|
||||
|
||||

|
||||
请参考 [GeekAI 项目介绍](https://docs.geekai.me/info/)。
|
||||
|
||||
---
|
||||
|
||||
_让 AI 成为你最强大的创作伙伴,开启无限创意可能!_
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
tmp
|
||||
bin
|
||||
data
|
||||
config.toml
|
||||
config.toml.*.bak
|
||||
static/upload
|
||||
static/audio
|
||||
storage.json
|
||||
res/certs/wechat/apiclient_key.pem
|
||||
@@ -0,0 +1,15 @@
|
||||
SHELL=/usr/bin/env bash
|
||||
NAME := geekai
|
||||
all: amd64 arm64
|
||||
|
||||
amd64:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/$(NAME)-linux main.go
|
||||
.PHONY: amd64
|
||||
|
||||
arm64:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -ldflags "-s -w" -o bin/$(NAME)-linux main.go
|
||||
.PHONY: arm64
|
||||
|
||||
clean:
|
||||
rm -rf bin/$(NAME)-*
|
||||
.PHONY: clean
|
||||
@@ -0,0 +1,5 @@
|
||||
# chatgpt-plus-go
|
||||
|
||||
chatgpt-plus 后端 API Go 语言实现。技术选型采用 Gin + Mysql 架构,依赖注入使用的是 fx 框架,ORM 采用的是 GORM 框架。
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
Listen = "0.0.0.0:5678"
|
||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
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"
|
||||
|
||||
[Session]
|
||||
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
|
||||
MaxAge = 86400
|
||||
|
||||
[Redis] # redis 配置信息
|
||||
Host = "localhost"
|
||||
Port = 6379
|
||||
Password = ""
|
||||
DB = 0
|
||||
@@ -0,0 +1,97 @@
|
||||
package core
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * 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/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppServer struct {
|
||||
Config *types.AppConfig
|
||||
Engine *gin.Engine
|
||||
SysConfig *types.SystemConfig // system config cache
|
||||
Redis *redis.Client
|
||||
}
|
||||
|
||||
func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types.SystemConfig) *AppServer {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
gin.DefaultWriter = io.Discard
|
||||
return &AppServer{
|
||||
Config: appConfig,
|
||||
Redis: redis,
|
||||
Engine: gin.Default(),
|
||||
SysConfig: sysConfig,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
// load system configs
|
||||
var sysConfig model.Config
|
||||
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.Value, &s.SysConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode system config: %v", err)
|
||||
}
|
||||
|
||||
// 统计安装信息
|
||||
go func() {
|
||||
info, err := host.Info()
|
||||
if err == nil {
|
||||
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.Debugf("register install info success: %v", resp.String())
|
||||
}
|
||||
}
|
||||
}()
|
||||
logger.Infof("http://%s", s.Config.Listen)
|
||||
return s.Engine.Run(s.Config.Listen)
|
||||
}
|
||||
|
||||
// 全局异常处理
|
||||
func errorHandler(c *gin.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Errorf("Handler Panic: %v", r)
|
||||
debug.PrintStack()
|
||||
c.JSON(http.StatusBadRequest, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
//加载完 defer recover,继续后续接口调用
|
||||
c.Next()
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package core
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * 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 (
|
||||
"bytes"
|
||||
"geekai/core/types"
|
||||
"geekai/log"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = log.GetLogger()
|
||||
|
||||
func NewDefaultConfig() *types.AppConfig {
|
||||
return &types.AppConfig{
|
||||
Listen: "0.0.0.0:5678",
|
||||
ProxyURL: "",
|
||||
StaticDir: "./static",
|
||||
StaticUrl: "http://localhost/5678/static",
|
||||
Redis: types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
|
||||
Session: types.Session{
|
||||
SecretKey: utils.RandString(64),
|
||||
MaxAge: 86400,
|
||||
},
|
||||
OSS: types.OSSConfig{
|
||||
Active: "local",
|
||||
Local: types.LocalStorageConfig{
|
||||
BaseURL: "http://localhost/5678/static/upload",
|
||||
BasePath: "./static/upload",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig(configFile string) (*types.AppConfig, error) {
|
||||
var config *types.AppConfig
|
||||
_, err := os.Stat(configFile)
|
||||
if err != nil {
|
||||
logger.Info("creating new config file: ", configFile)
|
||||
config = NewDefaultConfig()
|
||||
config.Path = configFile
|
||||
// save config
|
||||
err := SaveConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
_, err = toml.DecodeFile(configFile, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, err
|
||||
}
|
||||
|
||||
func SaveConfig(config *types.AppConfig) error {
|
||||
buf := new(bytes.Buffer)
|
||||
encoder := toml.NewEncoder(buf)
|
||||
if err := encoder.Encode(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
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
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// ApiRequest API 请求实体
|
||||
type ApiRequest struct {
|
||||
Model string `json:"model,omitempty"`
|
||||
Temperature float32 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // 兼容GPT O1 模型
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Messages []any `json:"messages,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
Functions []any `json:"functions,omitempty"` // 兼容中转平台
|
||||
ResponseFormat any `json:"response_format,omitempty"` // 响应格式
|
||||
|
||||
ToolChoice string `json:"tool_choice,omitempty"`
|
||||
|
||||
Input map[string]any `json:"input,omitempty"` //兼容阿里通义千问
|
||||
Parameters map[string]any `json:"parameters,omitempty"` //兼容阿里通义千问
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ApiResponse struct {
|
||||
Choices []ChoiceItem `json:"choices"`
|
||||
}
|
||||
|
||||
// ChoiceItem API 响应实体
|
||||
type ChoiceItem struct {
|
||||
Delta Delta `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type Delta struct {
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Content any `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
FunctionCall struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Arguments string `json:"arguments,omitempty"`
|
||||
} `json:"function_call,omitempty"`
|
||||
}
|
||||
|
||||
type ChatModel struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Description string `json:"description"` //模型描述
|
||||
Category string `json:"category"` //模型类别
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id"` // 绑定 API KEY
|
||||
}
|
||||
|
||||
type ApiError struct {
|
||||
Error struct {
|
||||
Message string
|
||||
Type string
|
||||
Param interface{}
|
||||
Code string
|
||||
}
|
||||
}
|
||||
|
||||
const PromptMsg = "prompt" // prompt message
|
||||
const ReplyMsg = "reply" // reply message
|
||||
|
||||
// PowerType 算力日志类型
|
||||
type PowerType int
|
||||
|
||||
const (
|
||||
PowerRecharge = PowerType(1) // 充值
|
||||
PowerConsume = PowerType(2) // 消费
|
||||
PowerRefund = PowerType(3) // 任务(SD,MJ)执行失败,退款
|
||||
PowerInvite = PowerType(4) // 邀请奖励
|
||||
PowerRedeem = PowerType(5) // 众筹
|
||||
PowerGift = PowerType(6) // 系统赠送
|
||||
PowerSignIn = PowerType(7) // 每日签到
|
||||
)
|
||||
|
||||
func (t PowerType) String() string {
|
||||
switch t {
|
||||
case PowerRecharge:
|
||||
return "充值"
|
||||
case PowerConsume:
|
||||
return "消费"
|
||||
case PowerRefund:
|
||||
return "退款"
|
||||
case PowerRedeem:
|
||||
return "兑换"
|
||||
case PowerGift:
|
||||
return "赠送"
|
||||
case PowerInvite:
|
||||
return "邀请"
|
||||
case PowerSignIn:
|
||||
return "签到"
|
||||
}
|
||||
return "其他"
|
||||
}
|
||||
|
||||
type PowerMark int
|
||||
|
||||
const (
|
||||
PowerSub = PowerMark(0)
|
||||
PowerAdd = PowerMark(1)
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
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 (
|
||||
"errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrConClosed = errors.New("connection Closed")
|
||||
|
||||
// WsClient websocket client
|
||||
type WsClient struct {
|
||||
Id string
|
||||
Conn *websocket.Conn
|
||||
lock sync.Mutex
|
||||
mt int
|
||||
Closed bool
|
||||
}
|
||||
|
||||
func NewWsClient(conn *websocket.Conn, id string) *WsClient {
|
||||
return &WsClient{
|
||||
Conn: conn,
|
||||
Id: id,
|
||||
lock: sync.Mutex{},
|
||||
mt: 2, // fixed bug for 'Invalid UTF-8 in text frame'
|
||||
Closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WsClient) Send(message []byte) error {
|
||||
wc.lock.Lock()
|
||||
defer wc.lock.Unlock()
|
||||
|
||||
if wc.Closed {
|
||||
return ErrConClosed
|
||||
}
|
||||
|
||||
return wc.Conn.WriteMessage(wc.mt, message)
|
||||
}
|
||||
|
||||
func (wc *WsClient) SendJson(value interface{}) error {
|
||||
wc.lock.Lock()
|
||||
defer wc.lock.Unlock()
|
||||
|
||||
if wc.Closed {
|
||||
return ErrConClosed
|
||||
}
|
||||
return wc.Conn.WriteJSON(value)
|
||||
}
|
||||
|
||||
func (wc *WsClient) Receive() (int, []byte, error) {
|
||||
if wc.Closed {
|
||||
return 0, nil, ErrConClosed
|
||||
}
|
||||
|
||||
return wc.Conn.ReadMessage()
|
||||
}
|
||||
|
||||
func (wc *WsClient) Close() {
|
||||
wc.lock.Lock()
|
||||
defer wc.lock.Unlock()
|
||||
|
||||
if wc.Closed {
|
||||
return
|
||||
}
|
||||
|
||||
_ = wc.Conn.Close()
|
||||
wc.Closed = true
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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 (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
Path string `toml:"-"`
|
||||
Listen string
|
||||
Session Session
|
||||
AdminSession Session
|
||||
ProxyURL 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 {
|
||||
Host string
|
||||
Port int
|
||||
Password string
|
||||
DB int
|
||||
}
|
||||
|
||||
func (c RedisConfig) Url() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
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"` //订单支付超时时间,单位:分钟
|
||||
|
||||
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"` // 可灵生成视频消耗算力
|
||||
AdvanceVoicePower int `json:"advance_voice_power,omitempty"` // 高级语音对话消耗算力
|
||||
|
||||
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
|
||||
|
||||
EnableContext bool `json:"enable_context,omitempty"`
|
||||
ContextDeep int `json:"context_deep,omitempty"`
|
||||
|
||||
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
||||
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
IndexPage string `json:"index_page"` // 首页显示的页面
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
GaBeian string `json:"ga_beian"` // 公安备案号
|
||||
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
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,27 @@
|
||||
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 ToolCall struct {
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
Function Function `json:"function"`
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
@@ -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,70 @@
|
||||
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 (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MKey interface {
|
||||
string | int | uint
|
||||
}
|
||||
type MValue interface {
|
||||
*WsClient | context.CancelFunc | []any
|
||||
}
|
||||
type LMap[K MKey, T MValue] struct {
|
||||
lock sync.RWMutex
|
||||
data map[K]T
|
||||
}
|
||||
|
||||
func NewLMap[K MKey, T MValue]() *LMap[K, T] {
|
||||
return &LMap[K, T]{
|
||||
lock: sync.RWMutex{},
|
||||
data: make(map[K]T),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) Put(key K, value T) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.data[key] = value
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) Get(key K) T {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
return m.data[key]
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) Has(key K) bool {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
_, ok := m.data[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) Delete(key K) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
delete(m.data, key)
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) ToList() []T {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
var s = make([]T, 0)
|
||||
for _, v := range m.data {
|
||||
s = append(s, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
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 OrderStatus int
|
||||
|
||||
const (
|
||||
OrderNotPaid = OrderStatus(0)
|
||||
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"`
|
||||
}
|
||||
|
||||
// PayChannel 支付渠道
|
||||
var PayChannel = map[string]string{
|
||||
"alipay": "支付宝商号",
|
||||
"wxpay": "微信商号",
|
||||
"epay": "易支付",
|
||||
}
|
||||
|
||||
var PayWays = map[string]string{
|
||||
"alipay": "支付宝",
|
||||
"wxpay": "微信支付",
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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 OSSConfig struct {
|
||||
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 `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 `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 `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 `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"` // 是否开启水印
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
const LoginUserID = "LOGIN_USER_ID"
|
||||
const AdminUserID = "ADMIN_USER_ID"
|
||||
const LoginUserCache = "LOGIN_USER_CACHE"
|
||||
|
||||
const UserAuthHeader = "Authorization"
|
||||
const AdminAuthHeader = "Admin-Authorization"
|
||||
|
||||
// Session configs struct
|
||||
type Session struct {
|
||||
SecretKey string
|
||||
MaxAge int
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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 SMSConfig struct {
|
||||
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 `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 `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
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// TaskType 任务类别
|
||||
type TaskType string
|
||||
|
||||
func (t TaskType) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
const (
|
||||
TaskImage = TaskType("image")
|
||||
TaskBlend = TaskType("blend")
|
||||
TaskSwapFace = TaskType("swapFace")
|
||||
TaskUpscale = TaskType("upscale")
|
||||
TaskVariation = TaskType("variation")
|
||||
TaskModal = TaskType("modal") // 局部重绘
|
||||
)
|
||||
|
||||
// MjTask MidJourney 任务
|
||||
type MjTask struct {
|
||||
Id uint `json:"id"` // 任务ID
|
||||
TaskId string `json:"task_id"` // 中转任务ID
|
||||
ImgArr []string `json:"img_arr"`
|
||||
Type TaskType `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
NegPrompt string `json:"neg_prompt,omitempty"`
|
||||
Params string `json:"full_prompt"`
|
||||
Index int `json:"index,omitempty"`
|
||||
MessageId string `json:"message_id,omitempty"`
|
||||
MessageHash string `json:"message_hash,omitempty"`
|
||||
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 使用)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Id uint `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
UserId int `json:"user_id"`
|
||||
Type int `json:"type"`
|
||||
Title string `json:"title"`
|
||||
RefTaskId string `json:"ref_task_id,omitempty"`
|
||||
RefSongId string `json:"ref_song_id,omitempty"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
Lyrics string `json:"lyrics,omitempty"` // 歌词
|
||||
Tags string `json:"tags"`
|
||||
Model string `json:"model"`
|
||||
Instrumental bool `json:"instrumental"` // 是否纯音乐
|
||||
ExtendSecs int `json:"extend_secs,omitempty"` // 延长秒杀
|
||||
SongId string `json:"song_id,omitempty"` // 合并歌曲ID
|
||||
AudioURL string `json:"audio_url"` // 用户上传音频地址
|
||||
}
|
||||
|
||||
const (
|
||||
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"` // 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" // 失败
|
||||
)
|
||||
@@ -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
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// BizVo 业务返回 VO
|
||||
type BizVo struct {
|
||||
Code BizCode `json:"code"`
|
||||
Page int `json:"page,omitempty"`
|
||||
PageSize int `json:"page_size,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// ReplyMessage 对话回复消息结构
|
||||
type ReplyMessage struct {
|
||||
Channel WsChannel `json:"channel"` // 消息频道,目前只有 chat
|
||||
ClientId string `json:"clientId"` // 客户端ID
|
||||
Type WsMsgType `json:"type"` // 消息类别
|
||||
Body interface{} `json:"body"`
|
||||
}
|
||||
|
||||
type WsMsgType string
|
||||
type WsChannel string
|
||||
|
||||
const (
|
||||
MsgTypeText = WsMsgType("text") // 输出内容
|
||||
MsgTypeEnd = WsMsgType("end")
|
||||
MsgTypeErr = WsMsgType("error")
|
||||
MsgTypePing = WsMsgType("ping") // 心跳消息
|
||||
|
||||
ChPing = WsChannel("ping")
|
||||
ChChat = WsChannel("chat")
|
||||
ChMj = WsChannel("mj")
|
||||
ChImage = WsChannel("image")
|
||||
ChSuno = WsChannel("suno")
|
||||
ChLuma = WsChannel("luma")
|
||||
ChKeLing = WsChannel("keling")
|
||||
)
|
||||
|
||||
// InputMessage 对话输入消息结构
|
||||
type InputMessage struct {
|
||||
Channel WsChannel `json:"channel"` // 消息频道
|
||||
Type WsMsgType `json:"type"` // 消息类别
|
||||
Body interface{} `json:"body"`
|
||||
}
|
||||
|
||||
type ChatMessage struct {
|
||||
Tools []int `json:"tools,omitempty"` // 允许调用工具列表
|
||||
Stream bool `json:"stream,omitempty"` // 是否采用流式输出
|
||||
RoleId int `json:"role_id"`
|
||||
ModelId int `json:"model_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type BizCode int
|
||||
|
||||
const (
|
||||
Success = BizCode(0)
|
||||
Failed = BizCode(1)
|
||||
NotAuthorized = BizCode(401) // 未授权
|
||||
|
||||
OkMsg = "Success"
|
||||
ErrorMsg = "系统开小差了"
|
||||
InvalidArgs = "非法参数或参数解析失败"
|
||||
)
|
||||
@@ -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
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
module geekai
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.1.0
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/imroc/req/v3 v3.37.2
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
|
||||
github.com/minio/minio-go/v7 v7.0.62
|
||||
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/go-pay/gopay v1.5.101
|
||||
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
|
||||
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.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/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 (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
||||
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-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
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
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.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
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/quic-go v0.45.0 // indirect
|
||||
github.com/refraction-networking/utls v1.3.2 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
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.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
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
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.7.0 // indirect
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
gorm.io/gorm v1.25.1
|
||||
)
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
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=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
||||
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
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-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=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-pay/crypto v0.0.1 h1:B6InT8CLfSLc6nGRVx9VMJRBBazFMjr293+jl0lLXUY=
|
||||
github.com/go-pay/crypto v0.0.1/go.mod h1:41oEIvHMKbNcYlWUlRWtsnC6+ASgh7u29z0gJXe5bes=
|
||||
github.com/go-pay/errgroup v0.0.2 h1:5mZMdm0TDClDm2S3G0/sm0f8AuQRtz0dOrTHDR9R8Cc=
|
||||
github.com/go-pay/errgroup v0.0.2/go.mod h1:0+4b8mvFMS71MIzsaC+gVvB4x37I93lRb2dqrwuU8x8=
|
||||
github.com/go-pay/gopay v1.5.101 h1:rVb+sfv6hiQtknAlZnTTLvU27NvFJ4p0yglN/vPpGXI=
|
||||
github.com/go-pay/gopay v1.5.101/go.mod h1:AW4Yj8jDZX9BM1/GTLTY1Gy5SHjiq8kQvG5sBTN2sxI=
|
||||
github.com/go-pay/util v0.0.2 h1:goJ4f6kNY5zzdtg1Cj8oWC+Cw7bfg/qq2rJangMAb9U=
|
||||
github.com/go-pay/util v0.0.2/go.mod h1:qM8VbyF1n7YAPZBSJONSPMPsPedhUTktewUAdf1AjPg=
|
||||
github.com/go-pay/xlog v0.0.2 h1:kUg5X8/5VZAPDg1J5eGjA3MG0/H5kK6Ew0dW/Bycsws=
|
||||
github.com/go-pay/xlog v0.0.2/go.mod h1:DbjMADPK4+Sjxj28ekK9goqn4zmyY4hql/zRiab+S9E=
|
||||
github.com/go-pay/xtime v0.0.2 h1:7YR4/iuELsEHpJ6LUO0SVK80hQxDO9MLCfuVYIiTCRM=
|
||||
github.com/go-pay/xtime v0.0.2/go.mod h1:W1yRbJaSt4CSBcdAtLBQ8xajiN/Pl5hquGczUcUE9xE=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
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-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=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
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/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=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
|
||||
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
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/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=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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=
|
||||
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=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc=
|
||||
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=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
|
||||
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
||||
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w=
|
||||
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=
|
||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/quic-go v0.45.0 h1:OHmkQGM37luZITyTSu6ff03HP/2IrwDX1ZFiNEhSFUE=
|
||||
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=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
|
||||
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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/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=
|
||||
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
|
||||
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/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=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8=
|
||||
go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
|
||||
go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA=
|
||||
go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM=
|
||||
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.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=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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.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.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.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=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
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.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.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=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.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=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
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.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.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=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
|
||||
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
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=
|
||||
@@ -0,0 +1,303 @@
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = log.GetLogger()
|
||||
|
||||
const SuperUsername = "admin"
|
||||
|
||||
type ManagerHandler struct {
|
||||
handler.BaseHandler
|
||||
redis *redis.Client
|
||||
captcha *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client, captcha *service.CaptchaService) *ManagerHandler {
|
||||
return &ManagerHandler{
|
||||
BaseHandler: handler.BaseHandler{DB: db, App: app},
|
||||
redis: client,
|
||||
captcha: captcha,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Dots string `json:"dots,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "请检查用户名或者密码是否填写正确")
|
||||
return
|
||||
}
|
||||
password := utils.GenPassword(data.Password, manager.Salt)
|
||||
if password != manager.Password {
|
||||
resp.ERROR(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 超级管理员默认是ID:1
|
||||
if manager.Username != SuperUsername && !manager.Status {
|
||||
resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": manager.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.AdminSession.SecretKey))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
key := fmt.Sprintf("admin/%d", manager.Id)
|
||||
if _, err := h.redis.Set(context.Background(), key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最后登录时间和IP
|
||||
manager.LastLoginIp = c.ClientIP()
|
||||
manager.LastLoginAt = time.Now().Unix()
|
||||
h.DB.Updates(&manager)
|
||||
|
||||
var result = struct {
|
||||
IsSuperAdmin bool `json:"is_super_admin"`
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
IsSuperAdmin: manager.Username == SuperUsername,
|
||||
Token: tokenString,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, result)
|
||||
}
|
||||
|
||||
// Logout 注销
|
||||
func (h *ManagerHandler) Logout(c *gin.Context) {
|
||||
key := h.GetUserKey(c)
|
||||
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
||||
logger.Error("error with delete session: ", err)
|
||||
} else {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Session 会话检测
|
||||
func (h *ManagerHandler) Session(c *gin.Context) {
|
||||
id := h.GetAdminId(c)
|
||||
if id == 0 {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
return
|
||||
}
|
||||
var manager model.AdminUser
|
||||
err := h.DB.Where("id", id).First(&manager).Error
|
||||
if err != nil {
|
||||
resp.NotAuth(c, "当前用户已退出登录")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, manager)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *ManagerHandler) List(c *gin.Context) {
|
||||
var items []model.AdminUser
|
||||
res := h.DB.Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
users := make([]vo.AdminUser, 0)
|
||||
for _, item := range items {
|
||||
var u vo.AdminUser
|
||||
err := utils.CopyObject(item, &u)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
u.Id = item.Id
|
||||
u.CreatedAt = item.CreatedAt.Unix()
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, users)
|
||||
|
||||
}
|
||||
|
||||
func (h *ManagerHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("username", data.Username).First(&user)
|
||||
if res.Error == nil {
|
||||
resp.ERROR(c, "用户名已存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成密码
|
||||
salt := utils.RandString(8)
|
||||
password := utils.GenPassword(data.Password, salt)
|
||||
res = h.DB.Save(&model.AdminUser{
|
||||
Username: data.Username,
|
||||
Password: password,
|
||||
Salt: salt,
|
||||
Status: data.Status,
|
||||
})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "failed with update database")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Remove 删除管理员
|
||||
func (h *ManagerHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
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{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Enable 启用/禁用
|
||||
func (h *ManagerHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.AdminUser{}).Where("id", data.Id).UpdateColumn("status", data.Enabled)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ResetPass 重置密码
|
||||
func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.DB.Model(&model.AdminUser{}).Where("id", data.Id).UpdateColumn("password", password)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ApiKeyHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
ApiURL string `json:"api_url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := model.ApiKey{}
|
||||
if data.Id > 0 {
|
||||
h.DB.Find(&apiKey, data.Id)
|
||||
}
|
||||
apiKey.Value = data.Value
|
||||
apiKey.Type = data.Type
|
||||
apiKey.ApiURL = data.ApiURL
|
||||
apiKey.Enabled = data.Enabled
|
||||
apiKey.ProxyURL = data.ProxyURL
|
||||
apiKey.Name = data.Name
|
||||
err := h.DB.Save(&apiKey).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var keyVo vo.ApiKey
|
||||
err = utils.CopyObject(apiKey, &keyVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("拷贝数据失败:%v", err))
|
||||
return
|
||||
}
|
||||
keyVo.Id = apiKey.Id
|
||||
keyVo.CreatedAt = apiKey.CreatedAt.Unix()
|
||||
resp.SUCCESS(c, keyVo)
|
||||
}
|
||||
|
||||
// List 获取 API KEY 列表
|
||||
func (h *ApiKeyHandler) List(c *gin.Context) {
|
||||
status := h.GetBool(c, "status")
|
||||
t := c.Query("type")
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if status {
|
||||
session = session.Where("enabled", true)
|
||||
}
|
||||
if t != "" {
|
||||
types := strings.Split(t, "|")
|
||||
session = session.Where("type IN ?", types)
|
||||
}
|
||||
|
||||
var items []model.ApiKey
|
||||
var keys = make([]vo.ApiKey, 0)
|
||||
res := session.Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var key vo.ApiKey
|
||||
err := utils.CopyObject(item, &key)
|
||||
if err == nil {
|
||||
key.Id = item.Id
|
||||
key.CreatedAt = item.CreatedAt.Unix()
|
||||
key.UpdatedAt = item.UpdatedAt.Unix()
|
||||
keys = append(keys, key)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, keys)
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.ApiKey{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) 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.ApiKey{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
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/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatAppHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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.ChatApp
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
err = h.DB.Save(&role).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 填充 ID 数据
|
||||
data.Id = role.Id
|
||||
data.CreatedAt = role.CreatedAt.Unix()
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
// initialize model mane for role
|
||||
modelIds := make([]int, 0)
|
||||
typeIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
if v.ModelId > 0 {
|
||||
modelIds = append(modelIds, int(v.ModelId))
|
||||
}
|
||||
if v.Tid > 0 {
|
||||
typeIds = append(typeIds, int(v.Tid))
|
||||
}
|
||||
}
|
||||
|
||||
modelNameMap := make(map[int]string)
|
||||
typeNameMap := make(map[int]string)
|
||||
if len(modelIds) > 0 {
|
||||
var models []model.ChatModel
|
||||
tx := h.DB.Where("id IN ?", modelIds).Find(&models)
|
||||
if tx.Error == nil {
|
||||
for _, m := range models {
|
||||
modelNameMap[int(m.Id)] = m.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(typeIds) > 0 {
|
||||
var appTypes []model.AppType
|
||||
tx := h.DB.Where("id IN ?", typeIds).Find(&appTypes)
|
||||
if tx.Error == nil {
|
||||
for _, m := range appTypes {
|
||||
typeNameMap[int(m.Id)] = m.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var role vo.ChatApp
|
||||
err := utils.CopyObject(v, &role)
|
||||
if err == nil {
|
||||
role.Id = v.Id
|
||||
role.CreatedAt = v.CreatedAt.Unix()
|
||||
role.UpdatedAt = v.UpdatedAt.Unix()
|
||||
role.ModelName = modelNameMap[int(role.ModelId)]
|
||||
role.TypeName = typeNameMap[int(role.Tid)]
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, roles)
|
||||
}
|
||||
|
||||
// Sort 更新角色排序
|
||||
func (h *ChatAppHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatAppHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatAppHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
res := h.DB.Where("id", id).Delete(&model.ChatApp{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "删除失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type ChatAppTypeHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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 {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Icon string `json:"icon"`
|
||||
SortNum int `json:"sort_num"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Id == 0 { // for add
|
||||
err := h.DB.Where("name", data.Name).First(&model.AppType{}).Error
|
||||
if err == nil {
|
||||
resp.ERROR(c, "当前分类已经存在")
|
||||
return
|
||||
}
|
||||
err = h.DB.Create(&model.AppType{
|
||||
Name: data.Name,
|
||||
Icon: data.Icon,
|
||||
Enabled: data.Enabled,
|
||||
SortNum: data.SortNum,
|
||||
}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
} else { // for update
|
||||
err := h.DB.Model(&model.AppType{}).Where("id", data.Id).Updates(map[string]interface{}{
|
||||
"name": data.Name,
|
||||
"icon": data.Icon,
|
||||
"enabled": data.Enabled,
|
||||
}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// List 获取App类型列表
|
||||
func (h *ChatAppTypeHandler) List(c *gin.Context) {
|
||||
var items []model.AppType
|
||||
var appTypes = make([]vo.AppType, 0)
|
||||
err := h.DB.Order("sort_num ASC").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var appType vo.AppType
|
||||
err = utils.CopyObject(v, &appType)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
appType.Id = v.Id
|
||||
appType.CreatedAt = v.CreatedAt.Unix()
|
||||
appTypes = append(appTypes, appType)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, appTypes)
|
||||
}
|
||||
|
||||
// Remove 删除App类型
|
||||
func (h *ChatAppTypeHandler) 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.AppType{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Enable 启用|禁用
|
||||
func (h *ChatAppTypeHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.AppType{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Sort 更新排序
|
||||
func (h *ChatAppTypeHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
err := h.DB.Model(&model.AppType{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
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/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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.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) {
|
||||
var data struct {
|
||||
Title string `json:"title"`
|
||||
UserId uint `json:"user_id"`
|
||||
Model string `json:"model"`
|
||||
CreateAt []string `json:"created_time"`
|
||||
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.Title != "" {
|
||||
session = session.Where("title LIKE ?", "%"+data.Title+"%")
|
||||
}
|
||||
if data.UserId > 0 {
|
||||
session = session.Where("user_id = ?", data.UserId)
|
||||
}
|
||||
if data.Model != "" {
|
||||
session = session.Where("model = ?", data.Model)
|
||||
}
|
||||
if len(data.CreateAt) == 2 {
|
||||
start := utils.Str2stamp(data.CreateAt[0] + " 00:00:00")
|
||||
end := utils.Str2stamp(data.CreateAt[1] + " 00:00:00")
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.ChatItem{}).Count(&total)
|
||||
var items []model.ChatItem
|
||||
var list = make([]chatItemVo, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
userIds := make([]uint, 0)
|
||||
chatIds := make([]string, 0)
|
||||
roleIds := make([]uint, 0)
|
||||
for _, item := range items {
|
||||
userIds = append(userIds, item.UserId)
|
||||
chatIds = append(chatIds, item.ChatId)
|
||||
roleIds = append(roleIds, item.RoleId)
|
||||
}
|
||||
var messages []model.ChatMessage
|
||||
var users []model.User
|
||||
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)
|
||||
|
||||
tokenMap := make(map[string]int)
|
||||
userMap := make(map[uint]string)
|
||||
msgMap := make(map[string]int)
|
||||
roleMap := make(map[uint]vo.ChatApp)
|
||||
for _, msg := range messages {
|
||||
tokenMap[msg.ChatId] += msg.Tokens
|
||||
msgMap[msg.ChatId] += 1
|
||||
}
|
||||
for _, user := range users {
|
||||
userMap[user.Id] = user.Username
|
||||
}
|
||||
for _, r := range roles {
|
||||
var roleVo vo.ChatApp
|
||||
err := utils.CopyObject(r, &roleVo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
roleMap[r.Id] = roleVo
|
||||
}
|
||||
for _, item := range items {
|
||||
list = append(list, chatItemVo{
|
||||
UserId: item.UserId,
|
||||
Username: userMap[item.UserId],
|
||||
ChatId: item.ChatId,
|
||||
Title: item.Title,
|
||||
Model: item.Model,
|
||||
Token: tokenMap[item.ChatId],
|
||||
MsgNum: msgMap[item.ChatId],
|
||||
Role: roleMap[item.RoleId],
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
type chatMessageVo struct {
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Model string `json:"model"`
|
||||
Token int `json:"token"`
|
||||
Icon string `json:"icon"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
// Messages 读取聊天记录列表
|
||||
func (h *ChatHandler) Messages(c *gin.Context) {
|
||||
var data struct {
|
||||
UserId uint `json:"user_id"`
|
||||
Content string `json:"content"`
|
||||
Model string `json:"model"`
|
||||
CreateAt []string `json:"created_time"`
|
||||
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.Content != "" {
|
||||
session = session.Where("content LIKE ?", "%"+data.Content+"%")
|
||||
}
|
||||
if data.UserId > 0 {
|
||||
session = session.Where("user_id = ?", data.UserId)
|
||||
}
|
||||
if data.Model != "" {
|
||||
session = session.Where("model = ?", data.Model)
|
||||
}
|
||||
if len(data.CreateAt) == 2 {
|
||||
start := utils.Str2stamp(data.CreateAt[0] + " 00:00:00")
|
||||
end := utils.Str2stamp(data.CreateAt[1] + " 00:00:00")
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.ChatMessage{}).Count(&total)
|
||||
var items []model.ChatMessage
|
||||
var list = make([]chatMessageVo, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
userIds := make([]uint, 0)
|
||||
for _, item := range items {
|
||||
userIds = append(userIds, item.UserId)
|
||||
}
|
||||
var users []model.User
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
userMap := make(map[uint]string)
|
||||
for _, user := range users {
|
||||
userMap[user.Id] = user.Username
|
||||
}
|
||||
for _, item := range items {
|
||||
list = append(list, chatMessageVo{
|
||||
Id: uint(item.Id),
|
||||
UserId: item.UserId,
|
||||
Username: userMap[item.UserId],
|
||||
Content: item.Content,
|
||||
Model: item.Model,
|
||||
Token: item.Tokens,
|
||||
Icon: item.Icon,
|
||||
Type: item.Type,
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
// History 获取聊天历史记录
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.ChatMessage, 0)
|
||||
res := h.DB.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.ChatMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(item.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = item.Content
|
||||
}
|
||||
v.Content = content
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, messages)
|
||||
}
|
||||
|
||||
// RemoveChat 删除对话
|
||||
func (h *ChatHandler) RemoveChat(c *gin.Context) {
|
||||
chatId := h.GetTrim(c, "chat_id")
|
||||
if chatId == "" {
|
||||
resp.ERROR(c, "请传入 ChatId")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
// 删除聊天记录
|
||||
res := tx.Unscoped().Debug().Where("chat_id = ?", chatId).Delete(&model.ChatMessage{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "failed to remove chat message")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除对话
|
||||
res = tx.Unscoped().Where("chat_id = ?", chatId).Delete(model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
tx.Rollback() // 回滚
|
||||
resp.ERROR(c, "failed to remove chat")
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// RemoveMessage 删除聊天记录
|
||||
func (h *ChatHandler) RemoveMessage(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
err := h.DB.Unscoped().Where("id = ?", id).Delete(&model.ChatMessage{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
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/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatModelHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortNum int `json:"sort_num"`
|
||||
Open bool `json:"open"`
|
||||
Platform string `json:"platform"`
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Desc string `json:"desc"` //模型描述
|
||||
Tag string `json:"tag"` //模型标签
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Type string `json:"type"`
|
||||
Options map[string]string `json:"options"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
item := model.ChatModel{}
|
||||
// 更新
|
||||
if data.Id > 0 {
|
||||
h.DB.Where("id", data.Id).First(&item)
|
||||
}
|
||||
|
||||
item.Name = data.Name
|
||||
item.Value = data.Value
|
||||
item.Enabled = data.Enabled
|
||||
item.Open = data.Open
|
||||
item.Power = data.Power
|
||||
item.MaxTokens = data.MaxTokens
|
||||
item.MaxContext = data.MaxContext
|
||||
item.Desc = data.Desc
|
||||
item.Tag = data.Tag
|
||||
item.Temperature = data.Temperature
|
||||
item.KeyId = uint(data.KeyId)
|
||||
item.Type = data.Type
|
||||
item.Options = utils.JsonEncode(data.Options)
|
||||
var res *gorm.DB
|
||||
if data.Id > 0 {
|
||||
res = h.DB.Save(&item)
|
||||
} else {
|
||||
res = h.DB.Create(&item)
|
||||
}
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.ChatModel
|
||||
err := utils.CopyObject(item, &itemVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "数据拷贝失败!")
|
||||
return
|
||||
}
|
||||
itemVo.Id = item.Id
|
||||
itemVo.CreatedAt = item.CreatedAt.Unix()
|
||||
resp.SUCCESS(c, itemVo)
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
enable := h.GetBool(c, "enable")
|
||||
name := h.GetTrim(c, "name")
|
||||
modelType := h.GetTrim(c, "type")
|
||||
if enable {
|
||||
session = session.Where("enabled", enable)
|
||||
}
|
||||
if name != "" {
|
||||
session = session.Where("name LIKE ?", name+"%")
|
||||
}
|
||||
if modelType != "" {
|
||||
session = session.Where("type", modelType)
|
||||
}
|
||||
var items []model.ChatModel
|
||||
var cms = make([]vo.ChatModel, 0)
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.SUCCESS(c, cms)
|
||||
return
|
||||
}
|
||||
|
||||
// initialize key name
|
||||
keyIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
keyIds = append(keyIds, int(v.KeyId))
|
||||
}
|
||||
var keys []model.ApiKey
|
||||
keyMap := make(map[uint]string)
|
||||
h.DB.Where("id IN ?", keyIds).Find(&keys)
|
||||
for _, v := range keys {
|
||||
keyMap[v.Id] = v.Name
|
||||
}
|
||||
for _, item := range items {
|
||||
var cm vo.ChatModel
|
||||
err := utils.CopyObject(item, &cm)
|
||||
if err == nil {
|
||||
cm.Id = item.Id
|
||||
cm.CreatedAt = item.CreatedAt.Unix()
|
||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||
cm.KeyName = keyMap[uint(item.KeyId)]
|
||||
cms = append(cms, cm)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, cms)
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
err := h.DB.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) 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.ChatModel{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
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 (
|
||||
"errors"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sms"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
handler.BaseHandler
|
||||
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,
|
||||
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},
|
||||
sysConfig: sysConfig,
|
||||
alipayService: alipayService,
|
||||
wxpayService: wxpayService,
|
||||
epayService: epayService,
|
||||
smsManager: smsManager,
|
||||
uploaderManager: uploaderManager,
|
||||
smtpService: smtpService,
|
||||
captchaService: captchaService,
|
||||
wxLoginService: wxLoginService,
|
||||
wxGzhService: wxChatService,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeySystem, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.sysConfig.Base = data
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) UpdateWxGzh(c *gin.Context) {
|
||||
var data types.WxGzhConfig
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
err := h.Update(types.ConfigKeyWxGzh, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.wxGzhService.UpdateConfig(data)
|
||||
h.sysConfig.WxGzh = data
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
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/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DashboardHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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"`
|
||||
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{}
|
||||
now := time.Now()
|
||||
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
// 总用户数
|
||||
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
|
||||
}
|
||||
|
||||
// 今日算力消耗
|
||||
var todayPowerLogs []model.PowerLog
|
||||
h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", zeroTime).Find(&todayPowerLogs)
|
||||
for _, item := range todayPowerLogs {
|
||||
stats.TodayTokens += item.Amount
|
||||
}
|
||||
|
||||
// 总收入
|
||||
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)
|
||||
//// 初始化
|
||||
var userStatistic, historyMessagesStatistic, incomeStatistic = make(map[string]float64), make(map[string]float64), make(map[string]float64)
|
||||
for i := 0; i < 7; i++ {
|
||||
var initTime = time.Date(now.Year(), now.Month(), now.Day()-i, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
|
||||
userStatistic[initTime] = float64(0)
|
||||
historyMessagesStatistic[initTime] = float64(0)
|
||||
incomeStatistic[initTime] = float64(0)
|
||||
}
|
||||
|
||||
// 统计用户7天增加的曲线
|
||||
var users []model.User
|
||||
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天算力消耗
|
||||
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天的订单
|
||||
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
|
||||
statsChart["historyMessage"] = historyMessagesStatistic
|
||||
statsChart["orders"] = incomeStatistic
|
||||
|
||||
stats.Chart = statsChart
|
||||
|
||||
resp.SUCCESS(c, stats)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
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/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FunctionHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var f = model.Function{
|
||||
Id: data.Id,
|
||||
Name: data.Name,
|
||||
Label: data.Label,
|
||||
Description: data.Description,
|
||||
Parameters: utils.JsonEncode(data.Parameters),
|
||||
Action: data.Action,
|
||||
Token: data.Token,
|
||||
Enabled: data.Enabled,
|
||||
}
|
||||
|
||||
res := h.DB.Save(&f)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "error with save data:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
data.Id = f.Id
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.Function{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) List(c *gin.Context) {
|
||||
var items []model.Function
|
||||
res := h.DB.Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No data found")
|
||||
return
|
||||
}
|
||||
|
||||
functions := make([]vo.Function, 0)
|
||||
for _, v := range items {
|
||||
var f vo.Function
|
||||
err := utils.CopyObject(v, &f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
functions = append(functions, f)
|
||||
}
|
||||
resp.SUCCESS(c, functions)
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
err := h.DB.Delete(&model.Function{Id: uint(id)}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// GenToken generate function api access token
|
||||
func (h *FunctionHandler) GenToken(c *gin.Context) {
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": 0,
|
||||
"expired": 0,
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
resp.ERROR(c)
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokenString)
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
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 ImageHandler struct {
|
||||
handler.BaseHandler
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewImageHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *ImageHandler {
|
||||
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"`
|
||||
CreatedAt []string `json:"created_at"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// MjList Midjourney 任务列表
|
||||
func (h *ImageHandler) MjList(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.MidJourneyJob{}).Count(&total)
|
||||
var list []model.MidJourneyJob
|
||||
var items = make([]vo.MidJourneyJob, 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.MidJourneyJob
|
||||
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))
|
||||
}
|
||||
|
||||
// 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)
|
||||
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.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.ImageJob
|
||||
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 *ImageHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
tab := c.Query("tab")
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var md, remark, imgURL string
|
||||
var power, userId, progress int
|
||||
switch tab {
|
||||
case "mj":
|
||||
var job model.MidJourneyJob
|
||||
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
tx.Delete(&job)
|
||||
md = "mid-journey"
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
case "image":
|
||||
var job model.ImageJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
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
|
||||
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(imgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -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": "配置更新成功"})
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
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/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MenuHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
URL string `json:"url"`
|
||||
SortNum int `json:"sort_num"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Save(&model.Menu{
|
||||
Id: data.Id,
|
||||
Name: data.Name,
|
||||
Icon: data.Icon,
|
||||
URL: data.URL,
|
||||
SortNum: data.SortNum,
|
||||
Enabled: data.Enabled,
|
||||
}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *MenuHandler) List(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)
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
err := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
err := h.DB.Where("id", id).Delete(&model.Menu{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
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/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OrderHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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"`
|
||||
Status int `json:"status"`
|
||||
PayTime []string `json:"pay_time"`
|
||||
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.OrderNo != "" {
|
||||
session = session.Where("order_no", data.OrderNo)
|
||||
}
|
||||
if len(data.PayTime) == 2 {
|
||||
start := utils.Str2stamp(data.PayTime[0] + " 00:00:00")
|
||||
end := utils.Str2stamp(data.PayTime[1] + " 00:00:00")
|
||||
session = session.Where("pay_time >= ? AND pay_time <= ?", start, end)
|
||||
}
|
||||
if data.Status >= 0 {
|
||||
session = session.Where("status", data.Status)
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.Order{}).Count(&total)
|
||||
var items []model.Order
|
||||
var list = make([]vo.Order, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var order vo.Order
|
||||
err := utils.CopyObject(item, &order)
|
||||
if err == nil {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
payChannel, ok := types.PayChannel[item.Channel]
|
||||
if !ok {
|
||||
payChannel = item.Channel
|
||||
}
|
||||
payWays, ok := types.PayWays[item.PayWay]
|
||||
if !ok {
|
||||
payWays = item.PayWay
|
||||
}
|
||||
order.ChannelName = payChannel
|
||||
order.PayName = payWays
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
var item model.Order
|
||||
res := h.DB.First(&item, id)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
if item.Status == types.OrderPaidSuccess {
|
||||
resp.ERROR(c, "已支付订单不允许删除!")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Where("id = ?", id).Delete(&model.Order{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Clear(c *gin.Context) {
|
||||
var orders []model.Order
|
||||
err := h.DB.Where("status <> ?", 2).Where("pay_time", 0).Find(&orders).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
deleteIds := make([]uint, 0)
|
||||
for _, order := range orders {
|
||||
// 只删除超时的未支付订单
|
||||
if time.Now().After(order.CreatedAt.Add(time.Minute * time.Duration(h.App.SysConfig.Base.OrderPayTimeout))) {
|
||||
deleteIds = append(deleteIds, order.Id)
|
||||
}
|
||||
}
|
||||
err = h.DB.Where("id IN ?", deleteIds).Delete(&model.Order{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PowerLogHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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"`
|
||||
UserId uint `json:"userid"`
|
||||
Type int `json:"type"`
|
||||
Model string `json:"model"`
|
||||
Date []string `json:"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.Model != "" {
|
||||
session = session.Where("model", data.Model)
|
||||
}
|
||||
if data.Type > 0 {
|
||||
session = session.Where("type", data.Type)
|
||||
}
|
||||
if data.UserId > 0 {
|
||||
session = session.Where("user_id", data.UserId)
|
||||
}
|
||||
if data.Username != "" {
|
||||
session = session.Where("username", data.Username)
|
||||
}
|
||||
if len(data.Date) == 2 {
|
||||
start := data.Date[0] + " 00:00:00"
|
||||
end := data.Date[1] + " 00:00:00"
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.PowerLog{}).Count(&total)
|
||||
var items []model.PowerLog
|
||||
var list = make([]vo.PowerLog, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var log vo.PowerLog
|
||||
err := utils.CopyObject(item, &log)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log.Id = item.Id
|
||||
log.CreatedAt = item.CreatedAt.Unix()
|
||||
log.TypeStr = item.Type.String()
|
||||
list = append(list, log)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计消费算力总和
|
||||
var totalPower float64
|
||||
if len(data.Date) == 2 {
|
||||
session.Where("mark", 0).Select("SUM(amount) as total_sum").Scan(&totalPower)
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"data": vo.NewPage(total, data.Page, data.PageSize, list), "stat": totalPower})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
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/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Power int `json:"power"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
item := model.Product{
|
||||
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)
|
||||
}
|
||||
err := h.DB.Save(&item).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.Product
|
||||
err = utils.CopyObject(item, &itemVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "数据拷贝失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
itemVo.Id = item.Id
|
||||
itemVo.UpdatedAt = item.UpdatedAt.Unix()
|
||||
resp.SUCCESS(c, itemVo)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
var items []model.Product
|
||||
var list = make([]vo.Product, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Product
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
product.Id = item.Id
|
||||
product.CreatedAt = item.CreatedAt.Unix()
|
||||
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, product)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.Product{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
err := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
err := h.DB.Where("id", id).Delete(&model.Product{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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 (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RedeemHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
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)
|
||||
code := c.Query("code")
|
||||
status := h.GetInt(c, "status", -1)
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if code != "" {
|
||||
session = session.Where("code LIKE ?", "%"+code+"%")
|
||||
}
|
||||
if status >= 0 {
|
||||
session = session.Where("redeemed_at", status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.Redeem{}).Count(&total)
|
||||
var redeems []model.Redeem
|
||||
offset := (page - 1) * pageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&redeems).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
var items = make([]vo.Redeem, 0)
|
||||
userIds := make([]uint, 0)
|
||||
for _, v := range redeems {
|
||||
userIds = append(userIds, v.UserId)
|
||||
}
|
||||
var users []model.User
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
var userMap = make(map[uint]model.User)
|
||||
for _, u := range users {
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
for _, v := range redeems {
|
||||
var r vo.Redeem
|
||||
err = utils.CopyObject(v, &r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.Id = v.Id
|
||||
r.Username = userMap[v.UserId].Username
|
||||
r.CreatedAt = v.CreatedAt.Unix()
|
||||
items = append(items, r)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||||
}
|
||||
|
||||
// Export 导出 CVS 文件
|
||||
func (h *RedeemHandler) Export(c *gin.Context) {
|
||||
var data struct {
|
||||
Status int `json:"status"`
|
||||
Ids []int `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Status >= 0 {
|
||||
session = session.Where("redeemed_at", data.Status)
|
||||
}
|
||||
if len(data.Ids) > 0 {
|
||||
session = session.Where("id IN ?", data.Ids)
|
||||
}
|
||||
|
||||
var items []model.Redeem
|
||||
err := session.Order("id DESC").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头,告诉浏览器这是一个附件,需要下载
|
||||
c.Header("Prompt-Disposition", "attachment; filename=output.csv")
|
||||
c.Header("Prompt-Type", "text/csv")
|
||||
|
||||
// 创建一个 CSV writer
|
||||
writer := csv.NewWriter(c.Writer)
|
||||
|
||||
// 写入 CSV 文件的标题行
|
||||
headers := []string{"名称", "兑换码", "算力", "创建时间"}
|
||||
if err := writer.Write(headers); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 写入数据行
|
||||
records := make([][]string, 0)
|
||||
for _, item := range items {
|
||||
records = append(records, []string{item.Name, item.Code, fmt.Sprintf("%d", item.Power), item.CreatedAt.Format("2006-01-02 15:04:05")})
|
||||
}
|
||||
for _, record := range records {
|
||||
if err := writer.Write(record); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 确保所有数据都已写入响应
|
||||
writer.Flush()
|
||||
if err := writer.Error(); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) Create(c *gin.Context) {
|
||||
var data struct {
|
||||
Name string `json:"name"`
|
||||
Power int `json:"power"`
|
||||
Num int `json:"num"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
counter := 0
|
||||
codes := make([]string, 0)
|
||||
var errMsg = ""
|
||||
if data.Num > 0 {
|
||||
for i := 0; i < data.Num; i++ {
|
||||
code, err := utils.GenRedeemCode(32)
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
continue
|
||||
}
|
||||
err = h.DB.Create(&model.Redeem{
|
||||
Code: code,
|
||||
Name: data.Name,
|
||||
Power: data.Power,
|
||||
Enabled: true,
|
||||
}).Error
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
continue
|
||||
}
|
||||
codes = append(codes, code)
|
||||
counter++
|
||||
}
|
||||
}
|
||||
if counter == 0 {
|
||||
resp.ERROR(c, errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"counter": counter,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.Redeem{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) 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.Redeem{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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/handler"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
handler.BaseHandler
|
||||
uploaderManager *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
|
||||
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")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.Base.MaxFileSize > 0 && f.Size > int64(h.App.SysConfig.Base.MaxFileSize)*1024*1024 {
|
||||
resp.ERROR(c, "文件大小超过限制")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userId := 0
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: uint(userId),
|
||||
Name: file.Name,
|
||||
ObjKey: file.ObjKey,
|
||||
URL: file.URL,
|
||||
Ext: file.Ext,
|
||||
Size: file.Size,
|
||||
CreatedAt: time.Time{},
|
||||
})
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "error with update database: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, file)
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"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/xuri/excelize/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
handler.BaseHandler
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
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 用户列表
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
username := h.GetTrim(c, "username")
|
||||
mobile := h.GetTrim(c, "mobile")
|
||||
email := h.GetTrim(c, "email")
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
var items []model.User
|
||||
var users = make([]vo.User, 0)
|
||||
var total int64
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if username != "" {
|
||||
session = session.Where("username LIKE ?", "%"+username+"%")
|
||||
}
|
||||
if mobile != "" {
|
||||
session = session.Where("mobile LIKE ?", "%"+mobile+"%")
|
||||
}
|
||||
if email != "" {
|
||||
session = session.Where("email LIKE ?", "%"+email+"%")
|
||||
}
|
||||
|
||||
session.Model(&model.User{}).Count(&total)
|
||||
res := session.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var user vo.User
|
||||
err := utils.CopyObject(item, &user)
|
||||
if err == nil {
|
||||
user.Id = item.Id
|
||||
user.CreatedAt = item.CreatedAt.Unix()
|
||||
user.UpdatedAt = item.UpdatedAt.Unix()
|
||||
users = append(users, user)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
pageVo := vo.NewPage(total, page, pageSize, users)
|
||||
resp.SUCCESS(c, pageVo)
|
||||
}
|
||||
|
||||
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"`
|
||||
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 user = model.User{}
|
||||
var res *gorm.DB
|
||||
var userVo vo.User
|
||||
if data.Id > 0 { // 更新
|
||||
res = h.DB.Where("id", data.Id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "user not found")
|
||||
return
|
||||
}
|
||||
var oldPower = user.Power
|
||||
user.Username = data.Username
|
||||
user.Email = data.Email
|
||||
user.Mobile = data.Mobile
|
||||
user.Status = data.Status
|
||||
user.Vip = data.Vip
|
||||
user.Power = data.Power
|
||||
user.ChatModels = utils.JsonEncode(data.ChatModels)
|
||||
user.ExpiredTime = utils.Str2stamp(data.ExpiredTime)
|
||||
|
||||
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)
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
// 记录算力日志
|
||||
if oldPower != user.Power {
|
||||
mark := types.PowerAdd
|
||||
amount := user.Power - oldPower
|
||||
if oldPower > user.Power {
|
||||
mark = types.PowerSub
|
||||
amount = oldPower - user.Power
|
||||
}
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerGift,
|
||||
Amount: amount,
|
||||
Balance: user.Power,
|
||||
Mark: mark,
|
||||
Model: "管理员",
|
||||
Remark: fmt.Sprintf("后台管理员强制修改用户算力,修改前:%d,修改后:%d, 管理员ID:%d", oldPower, user.Power, h.GetLoginUserId(c)),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
// 如果禁用了用户,则将用户踢下线
|
||||
if user.Status == false {
|
||||
key := fmt.Sprintf("users/%v", user.Id)
|
||||
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
||||
logger.Error("error with delete session: ", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检查用户是否已经存在
|
||||
h.DB.Where("username", data.Username).First(&user)
|
||||
if user.Id > 0 {
|
||||
resp.ERROR(c, "用户名已存在")
|
||||
return
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
u := model.User{
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Mobile: data.Mobile,
|
||||
Email: data.Email,
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Power: data.Power,
|
||||
Status: true,
|
||||
ChatConfig: "{}",
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
}
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
res = h.DB.Create(&u)
|
||||
_ = utils.CopyObject(u, &userVo)
|
||||
userVo.Id = u.Id
|
||||
userVo.CreatedAt = u.CreatedAt.Unix()
|
||||
userVo.UpdatedAt = u.UpdatedAt.Unix()
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, userVo)
|
||||
}
|
||||
|
||||
// ResetPass 重置密码
|
||||
func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint
|
||||
Password string
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.First(&user, data.Id)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No user found")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c)
|
||||
} else {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UserHandler) Remove(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
ids := c.QueryArray("ids[]")
|
||||
if id != "" {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var err error
|
||||
for _, id = range ids {
|
||||
// 删除用户
|
||||
if err = tx.Where("id", id).Delete(&model.User{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除聊天记录
|
||||
if err = tx.Unscoped().Where("user_id = ?", id).Delete(&model.ChatItem{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除聊天历史记录
|
||||
if err = tx.Unscoped().Where("user_id = ?", id).Delete(&model.ChatMessage{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除登录日志
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.UserLoginLog{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除算力日志
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.PowerLog{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.InviteLog{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除众筹日志
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.Redeem{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除绘图任务
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
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 {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.VideoJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *UserHandler) LoginLog(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
var total int64
|
||||
h.DB.Model(&model.UserLoginLog{}).Count(&total)
|
||||
offset := (page - 1) * pageSize
|
||||
var items []model.UserLoginLog
|
||||
res := h.DB.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "获取数据失败")
|
||||
return
|
||||
}
|
||||
var logs []vo.UserLoginLog
|
||||
for _, v := range items {
|
||||
var log vo.UserLoginLog
|
||||
err := utils.CopyObject(v, &log)
|
||||
if err == nil {
|
||||
log.Id = v.Id
|
||||
log.CreatedAt = v.CreatedAt.Unix()
|
||||
logs = append(logs, log)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs))
|
||||
}
|
||||
|
||||
// GenLoginLink 生成登录链接
|
||||
func (h *UserHandler) GenLoginLink(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if err := h.DB.Where("id = ?", id).First(&user).Error; err != nil {
|
||||
resp.ERROR(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 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))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+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
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokenString)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/log"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var logger = log.GetLogger()
|
||||
|
||||
type BaseHandler struct {
|
||||
App *core.AppServer
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetTrim(c *gin.Context, key string) string {
|
||||
return strings.TrimSpace(c.Query(key))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int {
|
||||
return utils.IntValue(c.PostForm(key), defaultValue)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int {
|
||||
return utils.IntValue(c.Query(key), defaultValue)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 {
|
||||
return utils.FloatValue(c.Query(key))
|
||||
}
|
||||
func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 {
|
||||
return utils.FloatValue(c.PostForm(key))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
|
||||
return utils.BoolValue(c.Query(key))
|
||||
}
|
||||
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
|
||||
return utils.BoolValue(c.PostForm(key))
|
||||
}
|
||||
func (h *BaseHandler) GetUserKey(c *gin.Context) string {
|
||||
userId, ok := c.Get(types.LoginUserID)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("users/%v", userId)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
|
||||
userId, ok := c.Get(types.LoginUserID)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetLoginUser(c *gin.Context) (model.User, error) {
|
||||
value, exists := c.Get(types.LoginUserCache)
|
||||
if exists {
|
||||
return value.(model.User), nil
|
||||
}
|
||||
|
||||
userId, ok := c.Get(types.LoginUserID)
|
||||
if !ok {
|
||||
return model.User{}, errors.New("user not login")
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.Where("id", userId).First(&user)
|
||||
// 更新缓存
|
||||
if res.Error == nil {
|
||||
c.Set(types.LoginUserCache, user)
|
||||
}
|
||||
return user, res.Error
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
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/service"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CaptchaHandler struct {
|
||||
App *core.AppServer
|
||||
service *service.CaptchaService
|
||||
}
|
||||
|
||||
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())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if h.service.Check(data) {
|
||||
resp.SUCCESS(c)
|
||||
} else {
|
||||
resp.ERROR(c)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if h.service.SlideCheck(data) {
|
||||
resp.SUCCESS(c)
|
||||
} else {
|
||||
resp.ERROR(c)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatAppTypeHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
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
|
||||
var appTypes = make([]vo.AppType, 0)
|
||||
err := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var appType vo.AppType
|
||||
err = utils.CopyObject(v, &appType)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
appType.Id = v.Id
|
||||
appType.CreatedAt = v.CreatedAt.Unix()
|
||||
appTypes = append(appTypes, appType)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, appTypes)
|
||||
}
|
||||
@@ -0,0 +1,842 @@
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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"
|
||||
"geekai/utils/resp"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ChatEventStart = "start"
|
||||
ChatEventEnd = "end"
|
||||
ChatEventComplete = "complete"
|
||||
ChatEventError = "error"
|
||||
ChatEventMessageDelta = "message_delta"
|
||||
ChatEventTitle = "title"
|
||||
)
|
||||
|
||||
type ChatInput struct {
|
||||
UserId uint `json:"user_id"`
|
||||
RoleId uint `json:"role_id"`
|
||||
ModelId uint `json:"model_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Tools []uint `json:"tools"`
|
||||
Stream bool `json:"stream"`
|
||||
Files []vo.File `json:"files"`
|
||||
ChatModel model.ChatModel `json:"chat_model,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
|
||||
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,
|
||||
userService *service.UserService,
|
||||
soraService *sora.SoraService,
|
||||
moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
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) {
|
||||
// 设置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)
|
||||
if chat.Id > 0 {
|
||||
input.ModelId = chat.ModelId
|
||||
input.RoleId = chat.RoleId
|
||||
}
|
||||
|
||||
// 验证聊天角色
|
||||
var chatRole model.ChatApp
|
||||
err := h.DB.First(&chatRole, input.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
pushMessage(c, ChatEventError, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!")
|
||||
return
|
||||
}
|
||||
input.ChatRole = chatRole
|
||||
|
||||
// 获取模型信息
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", input.ModelId).First(&chatModel).Error
|
||||
if err != nil || !chatModel.Enabled {
|
||||
pushMessage(c, ChatEventError, "当前AI模型暂未启用,请更换模型后再发起对话!")
|
||||
return
|
||||
}
|
||||
input.ChatModel = chatModel
|
||||
|
||||
// 发送消息
|
||||
err = h.sendMessage(ctx, input, c)
|
||||
if err != nil {
|
||||
pushMessage(c, ChatEventError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pushMessage(c, ChatEventEnd, "对话完成")
|
||||
}
|
||||
|
||||
func pushMessage(c *gin.Context, msgType string, content interface{}) {
|
||||
c.SSEvent("message", map[string]interface{}{
|
||||
"type": msgType,
|
||||
"body": content,
|
||||
})
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.Context) error {
|
||||
var user model.User
|
||||
res := h.DB.Model(&model.User{}).First(&user, input.UserId)
|
||||
if res.Error != nil {
|
||||
return errors.New("未授权用户,您正在进行非法操作!")
|
||||
}
|
||||
var userVo vo.User
|
||||
err := utils.CopyObject(user, &userVo)
|
||||
userVo.Id = user.Id
|
||||
if err != nil {
|
||||
return errors.New("User 对象转换失败," + err.Error())
|
||||
}
|
||||
|
||||
if !userVo.Status {
|
||||
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||
}
|
||||
|
||||
if userVo.Power < input.ChatModel.Power {
|
||||
return fmt.Errorf("您的算力不足,请购买算力。")
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
return errors.New("您的账号已经过期,请联系管理员!")
|
||||
}
|
||||
|
||||
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
|
||||
promptTokens, _ := utils.CalcTokens(input.Prompt, input.ChatModel.Value)
|
||||
if promptTokens > input.ChatModel.MaxContext {
|
||||
|
||||
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
|
||||
}
|
||||
|
||||
var req = types.ApiRequest{
|
||||
Model: input.ChatModel.Value,
|
||||
Stream: input.Stream,
|
||||
Temperature: input.ChatModel.Temperature,
|
||||
}
|
||||
// 兼容 OpenAI 模型
|
||||
if strings.HasPrefix(input.ChatModel.Value, "o1-") ||
|
||||
strings.HasPrefix(input.ChatModel.Value, "o3-") ||
|
||||
strings.HasPrefix(input.ChatModel.Value, "gpt") {
|
||||
req.MaxCompletionTokens = input.ChatModel.MaxTokens
|
||||
} else {
|
||||
req.MaxTokens = input.ChatModel.MaxTokens
|
||||
}
|
||||
|
||||
if len(input.Tools) > 0 && !strings.HasPrefix(input.ChatModel.Value, "o1-") {
|
||||
var items []model.Function
|
||||
res = h.DB.Where("enabled", true).Where("id IN ?", input.Tools).Find(&items)
|
||||
if res.Error == nil {
|
||||
var tools = make([]types.Tool, 0)
|
||||
for _, v := range items {
|
||||
var parameters map[string]interface{}
|
||||
err = utils.JsonDecode(v.Parameters, ¶meters)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tool := types.Tool{
|
||||
Type: "function",
|
||||
Function: types.Function{
|
||||
Name: v.Name,
|
||||
Description: v.Description,
|
||||
Parameters: parameters,
|
||||
},
|
||||
}
|
||||
if v, ok := parameters["required"]; v == nil || !ok {
|
||||
tool.Function.Parameters["required"] = []string{}
|
||||
}
|
||||
tools = append(tools, tool)
|
||||
}
|
||||
|
||||
if len(tools) > 0 {
|
||||
req.Tools = tools
|
||||
req.ToolChoice = "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载聊天上下文
|
||||
chatCtx := make([]any, 0)
|
||||
messages := make([]any, 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 { // 重新生成和编辑逻辑
|
||||
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{})
|
||||
}
|
||||
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]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算当前请求的 token 总长度,确保不会超出最大上下文长度
|
||||
// MaxContextLength = Response + Tool + Prompt + Context
|
||||
tokens := req.MaxTokens // 最大响应长度
|
||||
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
|
||||
tokens += tks + promptTokens
|
||||
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
v := messages[i]
|
||||
tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model)
|
||||
// 上下文 token 超出了模型的最大上下文长度
|
||||
if tokens+tks >= input.ChatModel.MaxContext {
|
||||
break
|
||||
}
|
||||
|
||||
// 上下文的深度超出了模型的最大上下文深度
|
||||
if len(chatCtx) >= h.App.SysConfig.Base.ContextDeep {
|
||||
break
|
||||
}
|
||||
|
||||
tokens += tks
|
||||
chatCtx = append(chatCtx, v)
|
||||
}
|
||||
}
|
||||
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])
|
||||
}
|
||||
|
||||
fileContents := make([]string, 0) // 文件内容
|
||||
var finalPrompt = input.Prompt
|
||||
imgList := make([]any, 0)
|
||||
for _, file := range input.Files {
|
||||
logger.Debugf("detected file: %+v", 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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(fileContents) > 0 {
|
||||
finalPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(fileContents, "\n"), input.Prompt)
|
||||
tokens, _ := utils.CalcTokens(finalPrompt, req.Model)
|
||||
if tokens > input.ChatModel.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
} else {
|
||||
finalPrompt = input.Prompt
|
||||
}
|
||||
|
||||
if len(imgList) > 0 {
|
||||
imgList = append(imgList, map[string]any{
|
||||
"type": "text",
|
||||
"text": input.Prompt,
|
||||
})
|
||||
req.Messages = append(reqMgs, map[string]any{
|
||||
"role": "user",
|
||||
"content": imgList,
|
||||
})
|
||||
} else {
|
||||
req.Messages = append(reqMgs, map[string]any{
|
||||
"role": "user",
|
||||
"content": finalPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
return h.sendOpenAiMessage(req, userVo, ctx, input, c)
|
||||
}
|
||||
|
||||
// 判断一个 URL 是否图片链接
|
||||
func isImageURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validImageExts := map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".bmp": true,
|
||||
".webp": true,
|
||||
".svg": true,
|
||||
".ico": true,
|
||||
}
|
||||
return validImageExts[ext]
|
||||
}
|
||||
|
||||
// 判断是个链接是否是文本链接
|
||||
func isTextURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validTextExts := map[string]bool{
|
||||
".txt": true,
|
||||
".doc": true,
|
||||
".docx": true,
|
||||
".pdf": true,
|
||||
}
|
||||
return validTextExts[ext]
|
||||
}
|
||||
|
||||
// 判断是个链接是否是视频
|
||||
func isVideoURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validVideoExts := map[string]bool{
|
||||
".mp4": true,
|
||||
".avi": true,
|
||||
".mov": true,
|
||||
".wmv": true,
|
||||
}
|
||||
return validVideoExts[ext]
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
func (h *ChatHandler) Tokens(c *gin.Context) {
|
||||
var data struct {
|
||||
Text string `json:"text"`
|
||||
Model string `json:"model"`
|
||||
ChatId string `json:"chat_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
|
||||
if data.Text == "" && data.ChatId != "" {
|
||||
var item model.ChatMessage
|
||||
userId, _ := c.Get(types.LoginUserID)
|
||||
res := h.DB.Where("user_id = ?", userId).Where("chat_id = ?", data.ChatId).Last(&item)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, item.Tokens)
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := utils.CalcTokens(data.Text, data.Model)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokens)
|
||||
}
|
||||
|
||||
func getTotalTokens(req types.ApiRequest) int {
|
||||
encode := utils.JsonEncode(req.Messages)
|
||||
var items []map[string]interface{}
|
||||
err := utils.JsonDecode(encode, &items)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
tokens := 0
|
||||
for _, item := range items {
|
||||
content, ok := item["content"]
|
||||
if ok && !utils.IsEmptyValue(content) {
|
||||
t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
|
||||
if err == nil {
|
||||
tokens += t
|
||||
}
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// StopGenerate 停止生成
|
||||
func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
sessionId := c.Query("session_id")
|
||||
if h.ReqCancelFunc.Has(sessionId) {
|
||||
h.ReqCancelFunc.Get(sessionId)()
|
||||
h.ReqCancelFunc.Delete(sessionId)
|
||||
}
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// 发送请求到 OpenAI 服务器
|
||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||
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).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)
|
||||
}
|
||||
|
||||
if apiKey.Id == 0 {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
logger.Debugf("对话请求消息体:%+v", req)
|
||||
var apiURL string
|
||||
p, _ := url.Parse(apiKey.ApiURL)
|
||||
// 如果设置的是 BASE_URL 没有路径,则添加 /v1/chat/completions
|
||||
if p.Path == "" {
|
||||
apiURL = fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
|
||||
} else {
|
||||
apiURL = apiKey.ApiURL
|
||||
}
|
||||
// 创建 HttpClient 请求对象
|
||||
var client *http.Client
|
||||
requestBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request = request.WithContext(ctx)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if len(apiKey.ProxyURL) > 5 { // 使用代理
|
||||
proxy, _ := url.Parse(apiKey.ProxyURL)
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||
// 更新API KEY 最后使用时间
|
||||
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
return client.Do(request)
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, input ChatInput, promptTokens int, replyTokens int) {
|
||||
power := 1
|
||||
if input.ChatModel.Power > 0 {
|
||||
power = input.ChatModel.Power
|
||||
}
|
||||
|
||||
err := h.userService.DecreasePower(userVo.Id, power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: input.ChatModel.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", input.ChatModel.Name, promptTokens, replyTokens),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHandler) saveChatHistory(
|
||||
c *gin.Context,
|
||||
req types.ApiRequest,
|
||||
usage Usage,
|
||||
message types.Message,
|
||||
input ChatInput,
|
||||
userVo vo.User,
|
||||
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
|
||||
if usage.PromptTokens > 0 {
|
||||
promptTokens = usage.PromptTokens
|
||||
} else {
|
||||
promptTokens, _ = utils.CalcTokens(usage.Content, req.Model)
|
||||
}
|
||||
|
||||
historyUserMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
RoleId: input.RoleId,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: usage.Prompt,
|
||||
Files: input.Files,
|
||||
}),
|
||||
Tokens: promptTokens,
|
||||
TotalTokens: promptTokens,
|
||||
UseContext: true,
|
||||
Model: req.Model,
|
||||
}
|
||||
historyUserMsg.CreatedAt = promptCreatedAt
|
||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||
err := h.DB.Save(&historyUserMsg).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save prompt history message: ", err)
|
||||
}
|
||||
|
||||
// for reply
|
||||
// 计算本次对话消耗的总 token 数量
|
||||
if usage.CompletionTokens > 0 {
|
||||
replyTokens = usage.CompletionTokens
|
||||
totalTokens = usage.TotalTokens
|
||||
} else {
|
||||
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,
|
||||
RoleId: input.RoleId,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: input.ChatRole.Icon,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: message.Content,
|
||||
Files: files,
|
||||
}),
|
||||
Tokens: replyTokens,
|
||||
TotalTokens: totalTokens,
|
||||
UseContext: true,
|
||||
Model: req.Model,
|
||||
}
|
||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||
err = h.DB.Create(&historyReplyMsg).Error
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
err = h.DB.Where("chat_id = ?", input.ChatId).First(&chatItem).Error
|
||||
if err != nil {
|
||||
chatItem.ChatId = input.ChatId
|
||||
chatItem.UserId = userVo.Id
|
||||
chatItem.RoleId = input.RoleId
|
||||
chatItem.ModelId = input.ModelId
|
||||
if utf8.RuneCountInString(usage.Prompt) > 30 {
|
||||
chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..."
|
||||
} else {
|
||||
chatItem.Title = usage.Prompt
|
||||
}
|
||||
chatItem.Model = req.Model
|
||||
err = h.DB.Create(&chatItem).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save chat item: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TextToSpeech 文本生成语音
|
||||
func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
var data struct {
|
||||
ModelId int `json:"model_id"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
textHash := utils.Sha256(fmt.Sprintf("%d/%s", data.ModelId, data.Text))
|
||||
audioFile := fmt.Sprintf("%s/audio", h.App.Config.StaticDir)
|
||||
if _, err := os.Stat(audioFile); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(audioFile, 0755); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
audioFile = fmt.Sprintf("%s/%s.mp3", audioFile, textHash)
|
||||
if _, err := os.Stat(audioFile); err == nil {
|
||||
// 设置响应头
|
||||
c.Header("Prompt-Type", "audio/mpeg")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
|
||||
c.File(audioFile)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询模型
|
||||
var chatModel model.ChatModel
|
||||
err := h.DB.Where("id", data.ModelId).First(&chatModel).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "找不到语音模型")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用 DeepSeek 的 API 接口
|
||||
var apiKey model.ApiKey
|
||||
if chatModel.KeyId > 0 {
|
||||
h.DB.Where("id", chatModel.KeyId).First(&apiKey)
|
||||
}
|
||||
if apiKey.Id == 0 {
|
||||
h.DB.Where("type", "tts").Where("enabled", true).First(&apiKey)
|
||||
}
|
||||
if apiKey.Id == 0 {
|
||||
resp.ERROR(c, "no TTS API key, please import key")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("chatModel: %+v, apiKey: %+v", chatModel, apiKey)
|
||||
|
||||
// 调用 openai tts api
|
||||
config := openai.DefaultConfig(apiKey.Value)
|
||||
config.BaseURL = apiKey.ApiURL + "/v1"
|
||||
client := openai.NewClientWithConfig(config)
|
||||
voice := openai.VoiceAlloy
|
||||
var options map[string]string
|
||||
err = utils.JsonDecode(chatModel.Options, &options)
|
||||
if err == nil {
|
||||
voice = openai.SpeechVoice(options["voice"])
|
||||
}
|
||||
req := openai.CreateSpeechRequest{
|
||||
Model: openai.SpeechModel(chatModel.Value),
|
||||
Input: data.Text,
|
||||
Voice: voice,
|
||||
}
|
||||
|
||||
audioData, err := client.CreateSpeech(context.Background(), req)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 先将音频数据读取到内存
|
||||
audioBytes, err := io.ReadAll(audioData)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 保存到音频文件
|
||||
err = os.WriteFile(audioFile, audioBytes, 0644)
|
||||
if err != nil {
|
||||
logger.Error("failed to save audio file: ", err)
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Prompt-Type", "audio/mpeg")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
|
||||
|
||||
// 直接写入完整的音频数据到响应
|
||||
_, err = c.Writer.Write(audioBytes)
|
||||
if err != nil {
|
||||
logger.Error("写入音频数据到响应失败:", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// List 获取会话列表
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
logger.Info(h.GetLoginUserId(c))
|
||||
if !h.IsLogin(c) {
|
||||
resp.SUCCESS(c)
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var items = make([]vo.ChatItem, 0)
|
||||
var chats []model.ChatItem
|
||||
h.DB.Debug().Where("user_id", userId).Order("id DESC").Find(&chats)
|
||||
if len(chats) == 0 {
|
||||
resp.SUCCESS(c, items)
|
||||
return
|
||||
}
|
||||
|
||||
var roleIds = make([]uint, 0)
|
||||
var modelValues = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
roleIds = append(roleIds, chat.RoleId)
|
||||
modelValues = append(modelValues, chat.Model)
|
||||
}
|
||||
|
||||
var roles []model.ChatApp
|
||||
var models []model.ChatModel
|
||||
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)
|
||||
for _, role := range roles {
|
||||
roleMap[role.Id] = role
|
||||
}
|
||||
for _, m := range models {
|
||||
modelMap[m.Value] = m
|
||||
}
|
||||
for _, chat := range chats {
|
||||
var item vo.ChatItem
|
||||
err := utils.CopyObject(chat, &item)
|
||||
if err == nil {
|
||||
item.Id = chat.Id
|
||||
item.Icon = roleMap[chat.RoleId].Icon
|
||||
item.ModelId = modelMap[chat.Model].Id
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, items)
|
||||
}
|
||||
|
||||
// Update 更新会话标题
|
||||
func (h *ChatHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
ChatId string `json:"chat_id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
res := h.DB.Model(&model.ChatItem{}).Where("chat_id = ?", data.ChatId).UpdateColumn("title", data.Title)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Failed to update database")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// Clear 清空所有聊天记录
|
||||
func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
// 获取当前登录用户所有的聊天会话
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var chats []model.ChatItem
|
||||
res := h.DB.Where("user_id = ?", user.Id).Find(&chats)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No chats found")
|
||||
return
|
||||
}
|
||||
|
||||
var chatIds = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
chatIds = append(chatIds, chat.ChatId)
|
||||
}
|
||||
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
res = h.DB.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.ChatMessage{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error with delete chats: %+v", err)
|
||||
resp.ERROR(c, "Failed to remove chat from database.")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// History 获取聊天历史记录
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.ChatMessage, 0)
|
||||
res := h.DB.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.ChatMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(item.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = item.Content
|
||||
}
|
||||
v.Content = content
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, messages)
|
||||
}
|
||||
|
||||
// Remove 删除会话
|
||||
func (h *ChatHandler) Remove(c *gin.Context) {
|
||||
chatId := h.GetTrim(c, "chat_id")
|
||||
if chatId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Failed to update database")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除当前会话的聊天记录
|
||||
res = h.DB.Where("user_id = ? AND chat_id =?", user.Id, chatId).Delete(&model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Failed to remove chat from database.")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// Detail 对话详情,用户导出对话
|
||||
func (h *ChatHandler) Detail(c *gin.Context) {
|
||||
chatId := h.GetTrim(c, "chat_id")
|
||||
if utils.IsEmptyValue(chatId) {
|
||||
resp.ERROR(c, "Invalid chatId")
|
||||
return
|
||||
}
|
||||
|
||||
var chatItem model.ChatItem
|
||||
res := h.DB.Where("chat_id = ?", chatId).First(&chatItem)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No chat found")
|
||||
return
|
||||
}
|
||||
|
||||
// 填充角色名称
|
||||
var role model.ChatApp
|
||||
res = h.DB.Where("id", chatItem.RoleId).First(&role)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Role not found")
|
||||
return
|
||||
}
|
||||
|
||||
var chatItemVo vo.ChatItem
|
||||
err := utils.CopyObject(chatItem, &chatItemVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
chatItemVo.RoleName = role.Name
|
||||
resp.SUCCESS(c, chatItemVo)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatModelHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
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
|
||||
var chatModels = make([]vo.ChatModel, 0)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("enabled", true)
|
||||
t := c.Query("type")
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
} else {
|
||||
session = session.Where("type", "chat")
|
||||
}
|
||||
|
||||
session = session.Where("open", true)
|
||||
if h.IsLogin(c) && t == "chat" {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
var models []int
|
||||
err := utils.JsonDecode(user.ChatModels, &models)
|
||||
// 查询用户有权限访问的模型以及所有开放的模型
|
||||
if err == nil {
|
||||
session = session.Or("id IN ?", models)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var cm vo.ChatModel
|
||||
err := utils.CopyObject(item, &cm)
|
||||
if err == nil {
|
||||
cm.Id = item.Id
|
||||
cm.CreatedAt = item.CreatedAt.Unix()
|
||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||
chatModels = append(chatModels, cm)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, chatModels)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
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 (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
req2 "github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
type Usage struct {
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type OpenAIResVo struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int `json:"created"`
|
||||
Model string `json:"model"`
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
Choices []struct {
|
||||
Index int `json:"index"`
|
||||
Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
Logprobs interface{} `json:"logprobs"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
// OPenAI 消息发送实现
|
||||
func (h *ChatHandler) sendOpenAiMessage(
|
||||
req types.ApiRequest,
|
||||
userVo vo.User,
|
||||
ctx context.Context,
|
||||
input ChatInput,
|
||||
c *gin.Context) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, input, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Since(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", input.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("Content-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
|
||||
|
||||
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.Content == nil &&
|
||||
responseBody.Choices[0].Delta.ToolCalls == nil &&
|
||||
responseBody.Choices[0].Delta.ReasoningContent == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
pushMessage(c, "text", "抱歉😔😔😔,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, "text", 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, "text", reasoningContent)
|
||||
contents = append(contents, reasoningContent)
|
||||
} else if responseBody.Choices[0].Delta.Content != "" {
|
||||
finalContent := responseBody.Choices[0].Delta.Content
|
||||
if reasoning {
|
||||
finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Content)
|
||||
reasoning = false
|
||||
}
|
||||
contents = append(contents, utils.InterfaceToString(finalContent))
|
||||
pushMessage(c, "text", finalContent)
|
||||
}
|
||||
}
|
||||
} // end for
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", input.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)
|
||||
// 扣减算力
|
||||
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)
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
usage := Usage{
|
||||
Prompt: input.Prompt,
|
||||
Content: strings.Join(contents, ""),
|
||||
PromptTokens: 0,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: 0,
|
||||
}
|
||||
message.Content = usage.Content
|
||||
h.saveChatHistory(c, req, usage, message, input, 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.Content
|
||||
pushMessage(c, "text", content)
|
||||
respVo.Usage.Prompt = input.Prompt
|
||||
respVo.Usage.Content = content
|
||||
h.saveChatHistory(c, req, respVo.Usage, respVo.Choices[0].Message, input, userVo, promptCreatedAt, time.Now())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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 (
|
||||
"errors"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
BaseHandler
|
||||
uploaderManager *oss.UploaderManager
|
||||
sysConfig *types.SystemConfig
|
||||
}
|
||||
|
||||
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
|
||||
err := h.DB.Where("name", key).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
|
||||
}
|
||||
|
||||
if key == types.ConfigKeyWxGzh {
|
||||
delete(value, "secret")
|
||||
delete(value, "token")
|
||||
delete(value, "encoding_aes_key")
|
||||
}
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// GetOssThumbTemplate 获取当前存储引擎的缩略图模板
|
||||
func (h *ConfigHandler) GetOssThumbTemplate(c *gin.Context) {
|
||||
template := h.uploaderManager.GetThumbTemplate()
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"template": template,
|
||||
"active": h.sysConfig.OSS.Active,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/image"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FunctionHandler struct {
|
||||
BaseHandler
|
||||
uploadManager *oss.UploaderManager
|
||||
imageService *image.Service
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewFunctionHandler(
|
||||
server *core.AppServer,
|
||||
db *gorm.DB,
|
||||
config *types.AppConfig,
|
||||
manager *oss.UploaderManager,
|
||||
imageService *image.Service,
|
||||
userService *service.UserService) *FunctionHandler {
|
||||
return &FunctionHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
DB: db,
|
||||
},
|
||||
uploadManager: manager,
|
||||
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"`
|
||||
Data struct {
|
||||
Title string `json:"title"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Items []dataItem `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type dataItem struct {
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// check authorization
|
||||
func (h *FunctionHandler) checkAuth(c *gin.Context) error {
|
||||
tokenString := c.GetHeader(types.UserAuthHeader)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(h.App.Config.Session.SecretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with parse auth token: %v", err)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return errors.New("token is invalid")
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() {
|
||||
return errors.New("token is expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WeiBo 微博热搜
|
||||
func (h *FunctionHandler) WeiBo(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/weibo/fetch", types.GeekAPIURL)
|
||||
var res resVo
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer geekai-plus").
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, fmt.Sprintf("error http code status: %v", r.Status))
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
builder := make([]string, 0)
|
||||
builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
|
||||
for i, v := range res.Data.Items {
|
||||
builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark))
|
||||
}
|
||||
resp.SUCCESS(c, strings.Join(builder, "\n\n"))
|
||||
}
|
||||
|
||||
// ZaoBao 今日早报
|
||||
func (h *FunctionHandler) ZaoBao(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/zaobao/fetch", types.GeekAPIURL)
|
||||
var res resVo
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer geekai-plus").
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, fmt.Sprintf("%v", r.Err))
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
builder := make([]string, 0)
|
||||
builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt))
|
||||
for _, v := range res.Data.Items {
|
||||
builder = append(builder, v.Title)
|
||||
}
|
||||
builder = append(builder, res.Data.Title)
|
||||
resp.SUCCESS(c, strings.Join(builder, "\n\n"))
|
||||
}
|
||||
|
||||
// Image3 AI 图像生成
|
||||
func (h *FunctionHandler) Image3(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
|
||||
}
|
||||
|
||||
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)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "当前用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < chatModel.Power {
|
||||
resp.ERROR(c, "创建绘图任务失败,算力不足")
|
||||
return
|
||||
}
|
||||
|
||||
// create image task
|
||||
prompt := utils.InterfaceToString(params["prompt"])
|
||||
task := types.ImageTask{
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
ModelId: chatModel.Id,
|
||||
ModelValue: chatModel.Value,
|
||||
ModelName: chatModel.Name,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
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, "创建绘图任务失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task.Id = job.Id
|
||||
content, err := h.imageService.Image(task, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务执行失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: task.ModelName,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(job.Prompt, 10)),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "扣减算力失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
// List 获取所有的工具函数列表
|
||||
func (h *FunctionHandler) List(c *gin.Context) {
|
||||
var items []model.Function
|
||||
err := h.DB.Where("enabled", true).Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tools := make([]vo.Function, 0)
|
||||
for _, v := range items {
|
||||
var f vo.Function
|
||||
err = utils.CopyObject(v, &f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
f.Action = ""
|
||||
f.Token = ""
|
||||
tools = append(tools, f)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tools)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
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 LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/image"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ImageJobHandler struct {
|
||||
BaseHandler
|
||||
imageService *image.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
moderationManager *moderation.ServiceManager
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *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, "模型不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户剩余算力
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
if user.Power < chatModel.Power {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
task := types.ImageTask{
|
||||
UserId: uint(userId),
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Name,
|
||||
ModelValue: chatModel.Value,
|
||||
Image: data.Image,
|
||||
Prompt: data.Prompt,
|
||||
AspectRatio: data.AspectRatio,
|
||||
Size: data.Size,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
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 {
|
||||
resp.ERROR(c, "error with save job: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
task.Id = job.Id
|
||||
h.imageService.PushTask(task)
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Name,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with decrease power: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ImgWall 照片墙
|
||||
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)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 Image 任务列表
|
||||
func (h *ImageJobHandler) 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 获取任务列表
|
||||
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 {
|
||||
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.ImageJob{}).Count(&total)
|
||||
|
||||
var items []model.ImageJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, vo.Page{}
|
||||
}
|
||||
|
||||
var jobs = make([]vo.ImageJob, 0)
|
||||
for _, item := range items {
|
||||
var job vo.ImageJob
|
||||
err := utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return nil, vo.NewPage(total, page, pageSize, jobs)
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *ImageJobHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.ImageJob
|
||||
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 *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.ImageJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
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 {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var modelVos []vo.ChatModel
|
||||
for _, v := range models {
|
||||
var modelVo vo.ChatModel
|
||||
err := utils.CopyObject(v, &modelVo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
modelVo.Id = v.Id
|
||||
modelVos = append(modelVos, modelVo)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, modelVos)
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
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/middleware"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// InviteHandler 用户邀请
|
||||
type InviteHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
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)
|
||||
var inviteCode model.InviteCode
|
||||
res := h.DB.Where("user_id = ?", userId).First(&inviteCode)
|
||||
// 如果邀请码不存在,则创建一个
|
||||
if res.Error != nil {
|
||||
code := strings.ToUpper(utils.RandString(8))
|
||||
for {
|
||||
res = h.DB.Where("code = ?", code).First(&inviteCode)
|
||||
if res.Error != nil { // 不存在相同的邀请码则退出
|
||||
break
|
||||
}
|
||||
}
|
||||
inviteCode.UserId = userId
|
||||
inviteCode.Code = code
|
||||
h.DB.Create(&inviteCode)
|
||||
}
|
||||
|
||||
var codeVo vo.InviteCode
|
||||
err := utils.CopyObject(inviteCode, &codeVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "拷贝对象失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, codeVo)
|
||||
}
|
||||
|
||||
// List Log 用户邀请记录
|
||||
func (h *InviteHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
userId := h.GetLoginUserId(c)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("inviter_id = ?", userId)
|
||||
var total int64
|
||||
session.Model(&model.InviteLog{}).Count(&total)
|
||||
var items []model.InviteLog
|
||||
offset := (page - 1) * pageSize
|
||||
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))
|
||||
}
|
||||
|
||||
// Hits 访问邀请码
|
||||
func (h *InviteHandler) Hits(c *gin.Context) {
|
||||
code := c.Query("code")
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MarkMapHandler 生成思维导图
|
||||
type MarkMapHandler struct {
|
||||
BaseHandler
|
||||
clients *types.LMap[int, *types.WsClient]
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewMarkMapHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *MarkMapHandler {
|
||||
return &MarkMapHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
clients: types.NewLMap[int, *types.WsClient](),
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Prompt string `json:"prompt"`
|
||||
ModelId int `json:"model_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var user model.User
|
||||
err := h.DB.Where("id", userId).First(&user, userId).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with query user info")
|
||||
return
|
||||
}
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", data.ModelId).First(&chatModel).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with query chat model")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < chatModel.Power {
|
||||
resp.ERROR(c, fmt.Sprintf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power))
|
||||
return
|
||||
}
|
||||
|
||||
messages := make([]interface{}, 0)
|
||||
messages = append(messages, types.Message{Role: "system", Content: `
|
||||
你是一位非常优秀的思维导图助手, 你能帮助用户整理思路,根据用户提供的主题或内容,快速生成结构清晰,有条理的思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
|
||||
# Geek-AI 助手
|
||||
|
||||
## 完整的开源系统
|
||||
### 前端开源
|
||||
### 后端开源
|
||||
|
||||
## 支持各种大模型
|
||||
### OpenAI
|
||||
### Azure
|
||||
### 文心一言
|
||||
### 通义千问
|
||||
|
||||
## 集成多种收费方式
|
||||
### 支付宝
|
||||
### 微信
|
||||
|
||||
请直接生成结果,不要任何解释性语句。
|
||||
`})
|
||||
messages = append(messages, types.Message{Role: "user", Content: fmt.Sprintf("请生成一份有关【%s】一份思维导图,要求结构清晰,有条理", data.Prompt)})
|
||||
content, err := utils.SendOpenAIMessage(h.DB, messages, data.ModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("请求 OpenAI API 失败: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 扣减算力
|
||||
if chatModel.Power > 0 {
|
||||
err = h.userService.DecreasePower(userId, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with save power log, "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MenuHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
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")
|
||||
var items []model.Menu
|
||||
var list = make([]vo.Menu, 0)
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
session = session.Where("enabled", true)
|
||||
if index {
|
||||
session = session.Where("id IN ?", h.App.SysConfig.Base.IndexNavs)
|
||||
}
|
||||
res := session.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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
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/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MidJourneyHandler struct {
|
||||
BaseHandler
|
||||
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, moderationManager *moderation.ServiceManager) *MidJourneyHandler {
|
||||
return &MidJourneyHandler{
|
||||
snowflake: snowflake,
|
||||
mjService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
resp.NotAuth(c)
|
||||
return false
|
||||
}
|
||||
|
||||
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 创建一个绘画任务
|
||||
func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
var data struct {
|
||||
TaskType string `json:"task_type"`
|
||||
Prompt string `json:"prompt"`
|
||||
NegPrompt string `json:"neg_prompt"`
|
||||
Rate string `json:"rate"`
|
||||
Model string `json:"model"` // 模型
|
||||
Chaos int `json:"chaos"` // 创意度取值范围: 0-100
|
||||
Raw bool `json:"raw"` // 是否开启原始模型
|
||||
Seed int64 `json:"seed"` // 随机数
|
||||
Stylize int `json:"stylize"` // 风格化
|
||||
ImgArr []string `json:"img_arr"`
|
||||
Tile bool `json:"tile"` // 重复平铺
|
||||
Quality float32 `json:"quality"` // 画质
|
||||
Iw float32 `json:"iw"`
|
||||
CRef string `json:"cref"` //生成角色一致的图像
|
||||
SRef string `json:"sref"` //生成风格一致的图像
|
||||
Cw int `json:"cw"` // 参考程度
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 按任务类型计算所需算力
|
||||
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
|
||||
}
|
||||
if data.Seed > 0 && !strings.Contains(params, "--seed") {
|
||||
params += fmt.Sprintf(" --seed %d", data.Seed)
|
||||
}
|
||||
if data.Stylize > 0 && !strings.Contains(params, "--s") && !strings.Contains(params, "--stylize") {
|
||||
params += fmt.Sprintf(" --s %d", data.Stylize)
|
||||
}
|
||||
if data.Chaos > 0 && !strings.Contains(params, "--c") && !strings.Contains(params, "--chaos") {
|
||||
params += fmt.Sprintf(" --c %d", data.Chaos)
|
||||
}
|
||||
if len(data.ImgArr) > 0 && data.Iw > 0 {
|
||||
params += fmt.Sprintf(" --iw %.2f", data.Iw)
|
||||
}
|
||||
if data.Raw {
|
||||
params += " --style raw"
|
||||
}
|
||||
if data.Quality > 0 {
|
||||
params += fmt.Sprintf(" --q %.2f", data.Quality)
|
||||
}
|
||||
if data.Tile {
|
||||
params += " --tile "
|
||||
}
|
||||
if data.CRef != "" {
|
||||
params += fmt.Sprintf(" --cref %s", data.CRef)
|
||||
if data.Cw > 0 {
|
||||
params += fmt.Sprintf(" --cw %d", data.Cw)
|
||||
} else {
|
||||
params += " --cw 100"
|
||||
}
|
||||
}
|
||||
|
||||
if data.SRef != "" {
|
||||
params += fmt.Sprintf(" --sref %s", data.SRef)
|
||||
}
|
||||
if data.Model != "" && !strings.Contains(params, "--v") && !strings.Contains(params, "--niji") {
|
||||
params += fmt.Sprintf(" %s", data.Model)
|
||||
}
|
||||
|
||||
// 处理融图和换脸的提示词
|
||||
if data.TaskType == types.TaskSwapFace.String() || data.TaskType == types.TaskBlend.String() {
|
||||
params = fmt.Sprintf("%s:%s", data.TaskType, strings.Join(data.ImgArr, ","))
|
||||
}
|
||||
|
||||
// 如果本地图片上传的是相对地址,处理成绝对地址
|
||||
for k, v := range data.ImgArr {
|
||||
if !strings.HasPrefix(v, "http") {
|
||||
data.ImgArr[k] = fmt.Sprintf("http://localhost:5678/%s", strings.TrimLeft(v, "/"))
|
||||
}
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
// generate task id
|
||||
taskId, err := h.snowflake.Next(true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate task id: "+err.Error())
|
||||
return
|
||||
}
|
||||
task := types.MjTask{
|
||||
TaskId: taskId,
|
||||
Type: types.TaskType(data.TaskType),
|
||||
Prompt: data.Prompt,
|
||||
NegPrompt: data.NegPrompt,
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
ImgArr: data.ImgArr,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: data.TaskType,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
Prompt: fmt.Sprintf("%s %s", data.Prompt, params),
|
||||
Power: power,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
opt := "绘图"
|
||||
if data.TaskType == types.TaskBlend.String() {
|
||||
job.Prompt = "融图:" + strings.Join(data.ImgArr, ",")
|
||||
opt = "融图"
|
||||
} else if data.TaskType == types.TaskSwapFace.String() {
|
||||
job.Prompt = "换脸:" + strings.Join(data.ImgArr, ",")
|
||||
opt = "换脸"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("%s操作,任务ID:%s", opt, job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
type reqVo struct {
|
||||
Index int `json:"index"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
MessageHash string `json:"message_hash"`
|
||||
}
|
||||
|
||||
// Upscale send upscale command to MidJourney Bot
|
||||
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
var data reqVo
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
power := mjActionPower(h.App.SysConfig.Base.MjUpscalePower, 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)
|
||||
task := types.MjTask{
|
||||
Type: types.TaskUpscale,
|
||||
UserId: userId,
|
||||
ChannelId: data.ChannelId,
|
||||
Index: data.Index,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskUpscale.String(),
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
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)
|
||||
|
||||
// update user's power
|
||||
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("Upscale 操作,任务ID:%s", job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Variation send variation command to MidJourney Bot
|
||||
func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
var data reqVo
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
power := mjActionPower(h.App.SysConfig.Base.MjUpscalePower, 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)
|
||||
task := types.MjTask{
|
||||
Type: types.TaskVariation,
|
||||
UserId: userId,
|
||||
Index: data.Index,
|
||||
ChannelId: data.ChannelId,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
Mode: h.App.SysConfig.Base.MjMode,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskVariation.String(),
|
||||
ChannelId: data.ChannelId,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
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("Variation 操作,任务ID:%s", job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
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 获取 MJ 任务列表
|
||||
func (h *MidJourneyHandler) 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 *MidJourneyHandler) 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.MidJourneyJob{}).Count(&total)
|
||||
|
||||
var items []model.MidJourneyJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, vo.Page{}
|
||||
}
|
||||
|
||||
var jobs = make([]vo.MidJourneyJob, 0)
|
||||
for _, item := range items {
|
||||
var job vo.MidJourneyJob
|
||||
err := utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
return nil, vo.NewPage(total, page, pageSize, jobs)
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *MidJourneyHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
var job model.MidJourneyJob
|
||||
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// remove job
|
||||
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 *MidJourneyHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
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/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NetHandler struct {
|
||||
BaseHandler
|
||||
uploaderManager *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewNetHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *NetHandler {
|
||||
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 {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("upload file: ", file.Name)
|
||||
// cut the file name if it's too long
|
||||
if len(file.Name) > 100 {
|
||||
file.Name = file.Name[:90] + file.Ext
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: uint(userId),
|
||||
Name: file.Name,
|
||||
ObjKey: file.ObjKey,
|
||||
URL: file.URL,
|
||||
Ext: file.Ext,
|
||||
Size: file.Size,
|
||||
CreatedAt: time.Time{},
|
||||
})
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "error with update database: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, file)
|
||||
}
|
||||
|
||||
func (h *NetHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var items []model.File
|
||||
var files = make([]vo.File, 0)
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
session = session.Where("user_id = ?", userId)
|
||||
if len(data.Urls) > 0 {
|
||||
session = session.Where("url IN ?", data.Urls)
|
||||
}
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.File{}).Count(&total)
|
||||
|
||||
if data.Page > 0 && data.PageSize > 0 {
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
session = session.Offset(offset).Limit(data.PageSize)
|
||||
}
|
||||
err := session.Order("id desc").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var file vo.File
|
||||
err := utils.CopyObject(v, &file)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
file.CreatedAt = v.CreatedAt.Unix()
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, files))
|
||||
}
|
||||
|
||||
// Remove remove files
|
||||
func (h *NetHandler) Remove(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
id := h.GetInt(c, "id", 0)
|
||||
var file model.File
|
||||
tx := h.DB.Where("user_id = ? AND id = ?", userId, id).First(&file)
|
||||
if tx.Error != nil || file.Id == 0 {
|
||||
resp.ERROR(c, "file not existed")
|
||||
return
|
||||
}
|
||||
|
||||
// remove database
|
||||
tx = h.DB.Model(&model.File{}).Delete("id = ?", id)
|
||||
if tx.Error != nil || tx.RowsAffected == 0 {
|
||||
resp.ERROR(c, "failed to update database")
|
||||
return
|
||||
}
|
||||
// remove files
|
||||
objectKey := file.ObjKey
|
||||
if objectKey == "" {
|
||||
objectKey = file.URL
|
||||
}
|
||||
_ = h.uploaderManager.GetUploadHandler().Delete(objectKey)
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *NetHandler) Download(c *gin.Context) {
|
||||
fileUrl := c.Query("url")
|
||||
// 使用http工具下载文件
|
||||
if fileUrl == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 使用http.Get下载文件
|
||||
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
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
resp.ERROR(c, "error status:"+r.Status)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
_, _ = io.Copy(c.Writer, r.Body)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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 OrderHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
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)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
userId := h.GetLoginUserId(c)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id = ? AND status = ?", userId, types.OrderPaidSuccess)
|
||||
var total int64
|
||||
session.Model(&model.Order{}).Count(&total)
|
||||
var items []model.Order
|
||||
var list = make([]vo.Order, 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 order vo.Order
|
||||
err := utils.CopyObject(item, &order)
|
||||
if err == nil {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
payChannel, ok := types.PayChannel[item.Channel]
|
||||
if !ok {
|
||||
payChannel = item.PayWay
|
||||
}
|
||||
payWays, ok := types.PayWays[item.PayWay]
|
||||
if !ok {
|
||||
payWays = item.PayWay
|
||||
}
|
||||
order.ChannelName = payChannel
|
||||
order.PayName = payWays
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
|
||||
}
|
||||
|
||||
// Query 查询订单状态
|
||||
func (h *OrderHandler) Query(c *gin.Context) {
|
||||
orderNo := h.GetTrim(c, "order_no")
|
||||
var order model.Order
|
||||
res := h.DB.Where("order_no = ?", orderNo).First(&order)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Order not found")
|
||||
return
|
||||
}
|
||||
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
return
|
||||
}
|
||||
|
||||
var item model.Order
|
||||
h.DB.Where("order_no = ?", orderNo).First(&item)
|
||||
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
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 (
|
||||
"embed"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PayWay struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// PaymentHandler 支付服务回调 handler
|
||||
type PaymentHandler struct {
|
||||
BaseHandler
|
||||
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,
|
||||
geekPayService *payment.EPayService,
|
||||
wxpayService *payment.WxPayService,
|
||||
db *gorm.DB,
|
||||
userService *service.UserService,
|
||||
snowflake *service.Snowflake,
|
||||
fs embed.FS,
|
||||
sysConfig *types.SystemConfig) *PaymentHandler {
|
||||
return &PaymentHandler{
|
||||
alipayService: alipayService,
|
||||
epayService: geekPayService,
|
||||
wxpayService: wxpayService,
|
||||
snowflake: snowflake,
|
||||
userService: userService,
|
||||
fs: fs,
|
||||
lock: sync.Mutex{},
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
DB: db,
|
||||
},
|
||||
config: &sysConfig.Payment,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,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)
|
||||
return
|
||||
}
|
||||
|
||||
var product model.Product
|
||||
err := h.DB.Where("id", data.Pid).First(&product).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo, err := h.snowflake.Next(false)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
userId := h.GetLoginUserId(c)
|
||||
var user model.User
|
||||
err = h.DB.Where("id", userId).First(&user).Error
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
amount := product.Price
|
||||
var payURL, notifyURL string
|
||||
switch data.PayWay {
|
||||
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,
|
||||
TotalFee: fmt.Sprintf("%d", int(amount*100)),
|
||||
Subject: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
ClientIP: c.ClientIP(),
|
||||
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,
|
||||
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, "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
|
||||
}
|
||||
default:
|
||||
resp.ERROR(c, "不支持的支付渠道")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Power: product.Power,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
}
|
||||
order := model.Order{
|
||||
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, gin.H{"pay_url": payURL, "order_no": orderNo})
|
||||
}
|
||||
|
||||
// 支付成功处理
|
||||
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", info.OutTradeNo).First(&order).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with fetch order: %v", err)
|
||||
}
|
||||
|
||||
// 已支付订单,直接返回
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
var user model.User
|
||||
err = h.DB.First(&user, order.UserId).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with fetch user info: %v", err)
|
||||
}
|
||||
|
||||
var remark types.OrderRemark
|
||||
err = utils.JsonDecode(order.Remark, &remark)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with decode order remark: %v", err)
|
||||
}
|
||||
|
||||
// 增加用户算力
|
||||
err = h.userService.IncreasePower(order.UserId, remark.Power, model.PowerLog{
|
||||
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 = utils.Str2stamp(info.PayTime)
|
||||
order.Status = types.OrderPaidSuccess
|
||||
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)
|
||||
}
|
||||
|
||||
// 更新产品销量
|
||||
err = h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId).
|
||||
UpdateColumn("sales", gorm.Expr("sales + ?", 1)).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with update product sales: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AlipayNotify 支付宝支付回调
|
||||
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = h.paySuccess(orderInfo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// 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("收到易支付订单支付回调:%+v", params)
|
||||
// 检查支付状态, 如果未支付,则返回成功
|
||||
if params["trade_status"] != "TRADE_SUCCESS" {
|
||||
c.String(http.StatusOK, "success")
|
||||
return
|
||||
}
|
||||
|
||||
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.paySuccess(order)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// WxpayNotify 微信商户支付异步回调
|
||||
func (h *PaymentHandler) WxpayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
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.paySuccess(orderInfo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
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"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PowerLogHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
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"`
|
||||
Date []string `json:"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{})
|
||||
userId := h.GetLoginUserId(c)
|
||||
session = session.Where("user_id", userId)
|
||||
if data.Model != "" {
|
||||
session = session.Where("model", data.Model)
|
||||
}
|
||||
if len(data.Date) == 2 {
|
||||
start := data.Date[0] + " 00:00:00"
|
||||
end := data.Date[1] + " 00:00:00"
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.PowerLog{}).Count(&total)
|
||||
var items []model.PowerLog
|
||||
var list = make([]vo.PowerLog, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var log vo.PowerLog
|
||||
err := utils.CopyObject(item, &log)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log.Id = item.Id
|
||||
log.CreatedAt = item.CreatedAt.Unix()
|
||||
log.TypeStr = item.Type.String()
|
||||
list = append(list, log)
|
||||
}
|
||||
}
|
||||
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": "删除成功"})
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
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
|
||||
var list = make([]vo.Product, 0)
|
||||
res := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Product
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
product.Id = item.Id
|
||||
product.CreatedAt = item.CreatedAt.Unix()
|
||||
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, product)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
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/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 提示词生成 handler
|
||||
// 使用 AI 生成绘画指令,歌词,视频生成指令等
|
||||
|
||||
type PromptHandler struct {
|
||||
BaseHandler
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewPromptHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *PromptHandler {
|
||||
return &PromptHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
// Image 生成 AI 绘画提示词
|
||||
func (h *PromptHandler) Image(c *gin.Context) {
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
}
|
||||
|
||||
// Video 生成视频提示词
|
||||
func (h *PromptHandler) Video(c *gin.Context) {
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
}
|
||||
|
||||
// MetaPrompt 生成元提示词
|
||||
func (h *PromptHandler) MetaPrompt(c *gin.Context) {
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
messages := make([]interface{}, 0)
|
||||
messages = append(messages, types.Message{
|
||||
Role: "system",
|
||||
Content: service.MetaPromptTemplate,
|
||||
})
|
||||
messages = append(messages, types.Message{
|
||||
Role: "user",
|
||||
Content: "Task, Goal, or the Role to actor is:\n" + data.Prompt,
|
||||
})
|
||||
content, err := utils.SendOpenAIMessage(h.DB, messages, 0)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
}
|
||||
|
||||
func (h *PromptHandler) getPromptModel() string {
|
||||
if h.App.SysConfig.Base.AssistantModelId > 0 {
|
||||
var chatModel model.ChatModel
|
||||
h.DB.Where("id", h.App.SysConfig.Base.AssistantModelId).First(&chatModel)
|
||||
return chatModel.Value
|
||||
}
|
||||
return "gpt-4o"
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * 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
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// OpenAI Realtime API Relay Server
|
||||
|
||||
type RealtimeHandler struct {
|
||||
BaseHandler
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewRealtimeHandler(server *core.AppServer, db *gorm.DB, userService *service.UserService) *RealtimeHandler {
|
||||
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")
|
||||
md := c.Query("model")
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将 HTTP 协议升级为 Websocket 协议
|
||||
subProtocols := strings.Split(clientProtocols, ",")
|
||||
ws, err := (&websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
Subprotocols: subProtocols,
|
||||
}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
// 目前只针对 VIP 用户可以访问
|
||||
if !user.Vip {
|
||||
sendError(ws, "当前功能只针对 VIP 用户开放")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var apiKey model.ApiKey
|
||||
h.DB.Where("type", "realtime").Where("enabled", true).Order("last_used_at ASC").First(&apiKey)
|
||||
if apiKey.Id == 0 {
|
||||
sendError(ws, "管理员未配置 Realtime API KEY")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/v1/realtime?model=%s", apiKey.ApiURL, md)
|
||||
// 连接到真实的后端服务器,传入相同的子协议
|
||||
headers := http.Header{}
|
||||
// 修正子协议内容
|
||||
subProtocols[1] = "openai-insecure-api-key." + apiKey.Value
|
||||
if clientProtocols != "" {
|
||||
headers.Set("Sec-WebSocket-Protocol", strings.Join(subProtocols, ","))
|
||||
}
|
||||
backendConn, _, err := websocket.DefaultDialer.Dial(apiURL, headers)
|
||||
if err != nil {
|
||||
sendError(ws, "桥接后端 API 失败:"+err.Error())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
defer backendConn.Close()
|
||||
|
||||
// 确保协议一致性,如果失败返回
|
||||
if ws.Subprotocol() != backendConn.Subprotocol() {
|
||||
sendError(ws, "Websocket 子协议不匹配")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 更新API KEY 最后使用时间
|
||||
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 开始双向转发
|
||||
errorChan := make(chan error, 2)
|
||||
go relay(ws, backendConn, errorChan)
|
||||
go relay(backendConn, ws, errorChan)
|
||||
|
||||
// 等待其中一个连接关闭
|
||||
err = <-errorChan
|
||||
logger.Infof("Relay ended: %v", err)
|
||||
}
|
||||
|
||||
func relay(src, dst *websocket.Conn, errorChan chan error) {
|
||||
for {
|
||||
messageType, message, err := src.ReadMessage()
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
err = dst.WriteMessage(messageType, message)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendError(ws *websocket.Conn, message string) {
|
||||
err := ws.WriteJSON(map[string]string{"event_id": "event_01", "type": "error", "error": message})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI 实时语音对话,一次性对话
|
||||
func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
|
||||
var apiKey model.ApiKey
|
||||
err := h.DB.Session(&gorm.Session{}).Where("type", "realtime").Where("enabled", true).First(&apiKey).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("error with fetch OpenAI API KEY:%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户是否还有算力
|
||||
userId := h.GetLoginUserId(c)
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("error with fetch user:%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.Base.AdvanceVoicePower {
|
||||
resp.ERROR(c, "当前用户算力不足,无法使用该功能")
|
||||
return
|
||||
}
|
||||
|
||||
var response utils.OpenAIResponse
|
||||
client := req.C()
|
||||
if len(apiKey.ProxyURL) > 5 {
|
||||
client.SetProxyURL(apiKey.ApiURL)
|
||||
}
|
||||
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("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(types.ApiRequest{
|
||||
Model: "advanced-voice",
|
||||
Temperature: 0.9,
|
||||
MaxTokens: 1024,
|
||||
Stream: false,
|
||||
Messages: []interface{}{types.Message{
|
||||
Role: "user",
|
||||
Content: "实时语音通话",
|
||||
}},
|
||||
}).Post(apiURL)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("请求 OpenAI API失败:%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, fmt.Sprintf("请求 OpenAI API失败:%v", r.Status))
|
||||
return
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("解析API数据失败:%v, %s", err, string(body)))
|
||||
}
|
||||
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.Base.AdvanceVoicePower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "advanced-voice",
|
||||
Remark: "实时语音通话",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
replyText := utils.NormalizeAssistantContent(response.Choices[0].Message.Content)
|
||||
logger.Infof("Response: %v", replyText)
|
||||
|
||||
// 提取链接
|
||||
re := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
|
||||
links := re.FindAllStringSubmatch(replyText, -1)
|
||||
var url = ""
|
||||
if len(links) > 0 {
|
||||
url = links[0][2]
|
||||
}
|
||||
resp.SUCCESS(c, url)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RedeemHandler struct {
|
||||
BaseHandler
|
||||
lock sync.Mutex
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewRedeemHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *RedeemHandler {
|
||||
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"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
var item model.Redeem
|
||||
res := h.DB.Where("code", data.Code).First(&item)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "无效的兑换码!")
|
||||
return
|
||||
}
|
||||
|
||||
if !item.Enabled {
|
||||
resp.ERROR(c, "当前兑换码已被禁用!")
|
||||
return
|
||||
}
|
||||
|
||||
if item.RedeemedAt > 0 {
|
||||
resp.ERROR(c, "当前兑换码已使用,请勿重复使用!")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
err := h.userService.IncreasePower(userId, item.Power, model.PowerLog{
|
||||
Type: types.PowerRedeem,
|
||||
Model: "兑换码",
|
||||
Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新核销状态
|
||||
item.RedeemedAt = time.Now().Unix()
|
||||
item.UserId = userId
|
||||
err = tx.Updates(&item).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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/service"
|
||||
"geekai/service/sms"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
const CodeStorePrefix = "/verify/codes/"
|
||||
|
||||
type SmsHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
sms *sms.SmsManager
|
||||
smtp *service.SmtpService
|
||||
captchaService *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewSmsHandler(
|
||||
app *core.AppServer,
|
||||
client *redis.Client,
|
||||
sms *sms.SmsManager,
|
||||
smtp *service.SmtpService,
|
||||
captcha *service.CaptchaService) *SmsHandler {
|
||||
return &SmsHandler{
|
||||
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 发送验证码
|
||||
func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
var data struct {
|
||||
Receiver string `json:"receiver"` // 接收者
|
||||
Key string `json:"key"`
|
||||
Dots string `json:"dots,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
code := utils.RandomNumber(6)
|
||||
var err error
|
||||
if strings.Contains(data.Receiver, "@") { // email
|
||||
if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "email") {
|
||||
resp.ERROR(c, "系统已禁用邮箱注册!")
|
||||
return
|
||||
}
|
||||
// 检查邮箱后缀是否在白名单
|
||||
if len(h.App.SysConfig.Base.EmailWhiteList) > 0 {
|
||||
inWhiteList := false
|
||||
for _, suffix := range h.App.SysConfig.Base.EmailWhiteList {
|
||||
if strings.HasSuffix(data.Receiver, suffix) {
|
||||
inWhiteList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inWhiteList {
|
||||
resp.ERROR(c, "邮箱后缀不在白名单中")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = h.smtp.SendVerifyCode(data.Receiver, code)
|
||||
} else {
|
||||
if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "mobile") {
|
||||
resp.ERROR(c, "系统已禁用手机号注册!")
|
||||
return
|
||||
}
|
||||
err = h.sms.GetService().SendVerifyCode(data.Receiver, code)
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 存储验证码,等待后面注册验证
|
||||
_, err = h.redis.Set(c, CodeStorePrefix+data.Receiver, code, 0).Result()
|
||||
if err != nil {
|
||||
resp.ERROR(c, "验证码保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
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/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/suno"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SunoHandler struct {
|
||||
BaseHandler
|
||||
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, moderationManager *moderation.ServiceManager) *SunoHandler {
|
||||
return &SunoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Create(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Instrumental bool `json:"instrumental"`
|
||||
Lyrics string `json:"lyrics"`
|
||||
Model string `json:"model"`
|
||||
Tags string `json:"tags"`
|
||||
Title string `json:"title"`
|
||||
Type int `json:"type"`
|
||||
RefTaskId string `json:"ref_task_id"` // 续写的任务id
|
||||
ExtendSecs int `json:"extend_secs"` // 续写秒数
|
||||
RefSongId string `json:"ref_song_id"` // 续写的歌曲id
|
||||
SongId string `json:"song_id,omitempty"` // 要拼接的歌曲id
|
||||
AudioURL string `json:"audio_url,omitempty"` // 上传自己创作的歌曲
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
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.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.Base.SunoPower {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
|
||||
// 歌曲拼接
|
||||
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.Params.Instrumental
|
||||
data.Model = song.Params.Model
|
||||
data.Tags = song.Params.Tags
|
||||
}
|
||||
// 拼接歌词
|
||||
var refSong model.SunoJob
|
||||
if err := h.DB.Where("song_id = ?", data.RefSongId).First(&refSong).Error; err == nil {
|
||||
data.Prompt = fmt.Sprintf("%s\n%s", song.Prompt, refSong.Prompt)
|
||||
}
|
||||
}
|
||||
task := types.SunoTask{
|
||||
UserId: int(h.GetLoginUserId(c)),
|
||||
Type: data.Type,
|
||||
Title: data.Title,
|
||||
RefTaskId: data.RefTaskId,
|
||||
RefSongId: data.RefSongId,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Prompt: data.Prompt,
|
||||
Lyrics: data.Lyrics,
|
||||
Tags: data.Tags,
|
||||
Model: data.Model,
|
||||
Instrumental: data.Instrumental,
|
||||
SongId: data.SongId,
|
||||
AudioURL: data.AudioURL,
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
job := model.SunoJob{
|
||||
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 {
|
||||
resp.ERROR(c, tx.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
task.Id = job.Id
|
||||
h.sunoService.PushTask(task)
|
||||
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: job.Params.Model,
|
||||
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.Params.Model),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.SunoJob{}).Count(&total)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
var list []model.SunoJob
|
||||
err := session.Order("id desc").Find(&list).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 初始化续写关系
|
||||
songIds := make([]string, 0)
|
||||
for _, v := range list {
|
||||
if v.RefTaskId != "" {
|
||||
songIds = append(songIds, v.RefSongId)
|
||||
}
|
||||
}
|
||||
var tasks []model.SunoJob
|
||||
h.DB.Where("song_id IN ?", songIds).Find(&tasks)
|
||||
songMap := make(map[string]model.SunoJob)
|
||||
for _, t := range tasks {
|
||||
songMap[t.SongId] = t
|
||||
}
|
||||
// 转换为 VO
|
||||
items := make([]vo.SunoJob, 0)
|
||||
for _, v := range list {
|
||||
var item vo.SunoJob
|
||||
err = utils.CopyObject(v, &item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item.CreatedAt = v.CreatedAt.Unix()
|
||||
if s, ok := songMap[v.RefSongId]; ok {
|
||||
item.RefSong = map[string]interface{}{
|
||||
"id": s.Id,
|
||||
"title": s.Title,
|
||||
"cover": s.CoverURL,
|
||||
"audio": s.AudioURL,
|
||||
}
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.SunoJob
|
||||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 只有失败或者已完成的任务可以删除
|
||||
if !(job.Progress == service.FailTaskProgress || job.Progress == 100) {
|
||||
resp.ERROR(c, "只有失败和超时(10分钟)的任务才能删除!")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
err = h.DB.Delete(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.AudioURL)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
publish := h.GetBool(c, "publish")
|
||||
err := h.DB.Model(&model.SunoJob{}).Where("id", id).Where("user_id", userId).UpdateColumn("publish", publish).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Cover string `json:"cover"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Id == 0 || data.Title == "" || data.Cover == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var item model.SunoJob
|
||||
if err := h.DB.Where("id", data.Id).Where("user_id", userId).First(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
item.Title = data.Title
|
||||
item.CoverURL = data.Cover
|
||||
|
||||
if err := h.DB.Updates(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Detail 歌曲详情
|
||||
func (h *SunoHandler) Detail(c *gin.Context) {
|
||||
songId := c.Query("song_id")
|
||||
if songId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var item model.SunoJob
|
||||
if err := h.DB.Where("song_id", songId).First(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 读取用户信息
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", item.UserId).First(&user).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.SunoJob
|
||||
if err := utils.CopyObject(item, &itemVo); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
itemVo.CreatedAt = item.CreatedAt.Unix()
|
||||
itemVo.User = map[string]interface{}{
|
||||
"nickname": user.Nickname,
|
||||
"avatar": user.Avatar,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, itemVo)
|
||||
}
|
||||
|
||||
// Play 增加歌曲播放次数
|
||||
func (h *SunoHandler) Play(c *gin.Context) {
|
||||
songId := c.Query("song_id")
|
||||
if songId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
h.DB.Model(&model.SunoJob{}).Where("song_id", songId).UpdateColumn("play_times", gorm.Expr("play_times + ?", 1))
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TestHandler struct {
|
||||
App *core.AppServer
|
||||
db *gorm.DB
|
||||
snowflake *service.Snowflake
|
||||
js *payment.EPayService
|
||||
}
|
||||
|
||||
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) {
|
||||
//c.Header("Body-Type", "text/event-stream")
|
||||
//c.Header("Cache-Control", "no-cache")
|
||||
//c.Header("Connection", "keep-alive")
|
||||
//
|
||||
//
|
||||
//// 模拟实时数据更新
|
||||
//for i := 0; i < 10; i++ {
|
||||
// // 发送 SSE 数据
|
||||
// _, err := fmt.Fprintf(c.Writer, "data: %v\n\n", data)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// c.Writer.Flush() // 确保立即发送数据
|
||||
// time.Sleep(1 * time.Second) // 每秒发送一次数据
|
||||
//}
|
||||
//c.Abort()
|
||||
}
|
||||
|
||||
func (h *TestHandler) PostTest(c *gin.Context) {
|
||||
var data struct {
|
||||
Message string `json:"message"`
|
||||
UserId uint `json:"user_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 将参数存储在上下文中
|
||||
c.Set("data", data)
|
||||
c.Next()
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
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"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
BaseHandler
|
||||
searcher *xdb.Searcher
|
||||
redis *redis.Client
|
||||
levelDB *store.LevelDB
|
||||
captchaService *service.CaptchaService
|
||||
userService *service.UserService
|
||||
wxLoginService *service.WxLoginService
|
||||
ipSearcher *xdb.Searcher
|
||||
wechatService *service.WxGzhService
|
||||
}
|
||||
|
||||
func NewUserHandler(
|
||||
app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
searcher *xdb.Searcher,
|
||||
client *redis.Client,
|
||||
levelDB *store.LevelDB,
|
||||
captcha *service.CaptchaService,
|
||||
userService *service.UserService,
|
||||
wxLoginService *service.WxLoginService,
|
||||
wechatService *service.WxGzhService,
|
||||
ipSearcher *xdb.Searcher) *UserHandler {
|
||||
return &UserHandler{
|
||||
BaseHandler: BaseHandler{DB: db, App: app},
|
||||
searcher: searcher,
|
||||
redis: client,
|
||||
levelDB: levelDB,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Register user register
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
// parameters process
|
||||
var data struct {
|
||||
RegWay string `json:"reg_way"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Dots string `json:"dots,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 人机验证
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data.Password = strings.TrimSpace(data.Password)
|
||||
if len(data.Password) < 8 {
|
||||
resp.ERROR(c, "密码长度不能少于8个字符")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
var key string
|
||||
if data.RegWay == "email" {
|
||||
key = CodeStorePrefix + data.Email
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
} else if data.RegWay == "mobile" {
|
||||
key = CodeStorePrefix + data.Mobile
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
session = session.Where("mobile = ?", data.Mobile)
|
||||
user.Username = data.Mobile
|
||||
user.Mobile = data.Mobile
|
||||
} else if data.Email != "" {
|
||||
session = session.Where("email = ?", data.Email)
|
||||
user.Username = data.Email
|
||||
user.Email = data.Email
|
||||
} else if data.Username != "" {
|
||||
session = session.Where("username = ?", data.Username)
|
||||
}
|
||||
session.First(&item)
|
||||
if item.Id > 0 {
|
||||
resp.ERROR(c, "该用户名已经被注册")
|
||||
return
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Dots string `json:"dots,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "用户名不存在")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
if password != user.Password {
|
||||
resp.ERROR(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Status {
|
||||
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})
|
||||
}
|
||||
|
||||
// Logout 注 销
|
||||
func (h *UserHandler) Logout(c *gin.Context) {
|
||||
key := h.GetUserKey(c)
|
||||
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
||||
logger.Error("error with delete session: ", err)
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"url": qrCodeURL,
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
|
||||
// 查询微信登录状态
|
||||
func (h *UserHandler) GetWxLoginState(c *gin.Context) {
|
||||
state := c.Query("state")
|
||||
if state == "" {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
status, err := h.wxLoginService.GetLoginStatus(state)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if status.Status != service.LoginStatusSuccess {
|
||||
resp.SUCCESS(c, status)
|
||||
return
|
||||
}
|
||||
|
||||
// 登录成功
|
||||
var user model.User
|
||||
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, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
token, err := h.doLogin(&user, c.ClientIP())
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
status.Status = service.LoginStatusExpired
|
||||
h.wxLoginService.SetLoginStatus(state, *status)
|
||||
|
||||
status.Status = service.LoginStatusSuccess
|
||||
status.Token = token
|
||||
resp.SUCCESS(c, status)
|
||||
}
|
||||
|
||||
// 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("用户名或密码不能为空")
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
// 保存到 redis
|
||||
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
|
||||
}
|
||||
|
||||
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 获取/验证会话
|
||||
func (h *UserHandler) Session(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var userVo vo.User
|
||||
err = utils.CopyObject(user, &userVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 用户 VIP 到期
|
||||
if user.ExpiredTime > 0 && user.ExpiredTime < time.Now().Unix() {
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
type userProfile struct {
|
||||
Id uint `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
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) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
h.DB.First(&user, user.Id)
|
||||
var profile userProfile
|
||||
err = utils.CopyObject(user, &profile)
|
||||
if err != nil {
|
||||
logger.Error("对象拷贝失败:", err.Error())
|
||||
resp.ERROR(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (h *UserHandler) ProfileUpdate(c *gin.Context) {
|
||||
var data userProfile
|
||||
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
|
||||
}
|
||||
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, "更新用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// UpdatePass 更新密码
|
||||
func (h *UserHandler) UpdatePass(c *gin.Context) {
|
||||
var data struct {
|
||||
OldPass string `json:"old_pass"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if len(data.Password) < 8 {
|
||||
resp.ERROR(c, "密码长度不能少于8个字符")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.OldPass, user.Salt)
|
||||
logger.Debugf(user.Salt, ",", user.Password, ",", password, ",", data.OldPass)
|
||||
if password != user.Password {
|
||||
resp.ERROR(c, "原密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
newPass := utils.GenPassword(data.Password, user.Salt)
|
||||
err = h.DB.Model(&user).UpdateColumn("password", newPass).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ResetPass 找回密码
|
||||
func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
var data struct {
|
||||
Type string `json:"type"` // 验证类别:mobile, email
|
||||
Mobile string `json:"mobile"` // 手机号
|
||||
Email string `json:"email"` // 邮箱地址
|
||||
Code string `json:"code"` // 验证码
|
||||
Password string `json:"password"` // 新密码
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
var key string
|
||||
switch data.Type {
|
||||
case "email":
|
||||
session = session.Where("email", data.Email)
|
||||
key = CodeStorePrefix + data.Email
|
||||
case "mobile":
|
||||
session = session.Where("mobile", data.Mobile)
|
||||
key = CodeStorePrefix + data.Mobile
|
||||
default:
|
||||
resp.ERROR(c, "验证类别错误")
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
err := session.First(&user).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
err = h.DB.Model(&user).UpdateColumn("password", password).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
} else {
|
||||
h.redis.Del(c, key)
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BindMobile 绑定手机号
|
||||
func (h *UserHandler) BindMobile(c *gin.Context) {
|
||||
var data struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Mobile
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查手机号是否被其他账号绑定
|
||||
var item model.User
|
||||
res := h.DB.Where("mobile", data.Mobile).First(&item)
|
||||
if res.Error == nil {
|
||||
resp.ERROR(c, "该手机号已经绑定了其他账号,请更换手机号")
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
err = h.DB.Model(&item).Where("id", userId).UpdateColumn("mobile", data.Mobile).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.redis.Del(c, key) // 删除短信验证码
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// BindEmail 绑定邮箱
|
||||
func (h *UserHandler) BindEmail(c *gin.Context) {
|
||||
var data struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Email
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查手机号是否被其他账号绑定
|
||||
var item model.User
|
||||
res := h.DB.Where("email", data.Email).First(&item)
|
||||
if res.Error == nil {
|
||||
resp.ERROR(c, "该邮箱地址已经绑定了其他账号,请更邮箱地址")
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
err = h.DB.Model(&item).Where("id", userId).UpdateColumn("email", data.Email).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.redis.Del(c, key) // 删除短信验证码
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// SignIn 每日签到
|
||||
func (h *UserHandler) SignIn(c *gin.Context) {
|
||||
// 获取当前日期
|
||||
date := time.Now().Format("2006-01-02")
|
||||
|
||||
// 检查是否已经签到
|
||||
userId := h.GetLoginUserId(c)
|
||||
key := fmt.Sprintf("signin/%d/%s", userId, date)
|
||||
var signIn bool
|
||||
err := h.levelDB.Get(key, &signIn)
|
||||
if err == nil && signIn {
|
||||
resp.ERROR(c, "今日已签到,请明日再来!")
|
||||
return
|
||||
}
|
||||
|
||||
// 签到
|
||||
h.levelDB.Put(key, true)
|
||||
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.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})
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
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/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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type VideoHandler struct {
|
||||
BaseHandler
|
||||
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, moderationManager *moderation.ServiceManager) *VideoHandler {
|
||||
return &VideoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
videoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
moderationManager: moderationManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *VideoHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/video/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
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
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if data.Provider == "" {
|
||||
resp.ERROR(c, "provider 不能为空")
|
||||
return
|
||||
}
|
||||
if data.Model == "" {
|
||||
resp.ERROR(c, "model 不能为空")
|
||||
return
|
||||
}
|
||||
if data.Prompt == "" {
|
||||
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
|
||||
}
|
||||
|
||||
// 计算算力
|
||||
power, err := video.CalculatePower(h.DB, data.Model, data.PriceKey)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 检查算力是否充足
|
||||
if user.Power < power {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建任务
|
||||
userId := int(h.GetLoginUserId(c))
|
||||
task := types.VideoTask{
|
||||
UserId: userId,
|
||||
Type: data.Provider, // provider 作为 type
|
||||
Prompt: data.Prompt,
|
||||
Params: data.Params,
|
||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: uint(userId),
|
||||
Type: data.Provider,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
Params: 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)
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
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, 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) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
t := c.Query("type")
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
all := h.GetBool(c, "all")
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
if all {
|
||||
session = session.Where("publish", 0).Where("status", types.VideoStatusSuccess)
|
||||
} else {
|
||||
session = session.Where("user_id", userId)
|
||||
}
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.VideoJob{}).Count(&total)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
var list []model.VideoJob
|
||||
err := session.Order("id desc").Find(&list).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 VO
|
||||
items := make([]vo.VideoJob, 0)
|
||||
for _, v := range list {
|
||||
var item vo.VideoJob
|
||||
err = utils.CopyObject(v, &item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item.CreatedAt = v.CreatedAt.Unix()
|
||||
// 解析任务详情(用于前端展示标签)
|
||||
if v.Params != "" {
|
||||
task := types.VideoTask{}
|
||||
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)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||||
}
|
||||
|
||||
func (h *VideoHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.VideoJob
|
||||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 只有失败的任务才能删除
|
||||
if job.Status != types.VideoStatusFailed {
|
||||
resp.ERROR(c, "只有失败的任务才能删除!")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
err = h.DB.Delete(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *VideoHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
publish := h.GetBool(c, "publish")
|
||||
var job model.VideoJob
|
||||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = h.DB.Model(&job).UpdateColumn("publish", publish).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
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, "创建菜单成功")
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package log
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * 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 (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
var sugarLogger *zap.SugaredLogger
|
||||
|
||||
func GetLogger() *zap.SugaredLogger {
|
||||
if sugarLogger != nil {
|
||||
return sugarLogger
|
||||
}
|
||||
|
||||
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("GEEKAI_LOG_LEVEL")))
|
||||
encoder := getEncoder()
|
||||
writerSyncer := getLogWriter()
|
||||
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
|
||||
consoleOutput := zapcore.Lock(os.Stdout)
|
||||
consoleCore := zapcore.NewCore(
|
||||
encoder,
|
||||
consoleOutput,
|
||||
logLevel,
|
||||
)
|
||||
core := zapcore.NewTee(fileCore, consoleCore)
|
||||
logger = zap.New(core, zap.AddCaller())
|
||||
sugarLogger = logger.Sugar()
|
||||
return sugarLogger
|
||||
}
|
||||
|
||||
// core 三个参数之 编码
|
||||
func getEncoder() zapcore.Encoder {
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||
}
|
||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
||||
}
|
||||
|
||||
func getLogWriter() zapcore.WriteSyncer {
|
||||
lumberJackLogger := &lumberjack.Logger{
|
||||
Filename: "logs/app.log",
|
||||
MaxSize: 10,
|
||||
MaxBackups: 5,
|
||||
MaxAge: 30,
|
||||
Compress: false,
|
||||
}
|
||||
return zapcore.AddSync(lumberJackLogger)
|
||||
}
|
||||
|
||||
func getLogLevel(level string) zapcore.Level {
|
||||
switch strings.ToUpper(level) {
|
||||
case "DEBUG":
|
||||
return zapcore.DebugLevel
|
||||
case "WARN":
|
||||
return zapcore.WarnLevel
|
||||
case "ERROR":
|
||||
return zapcore.ErrorLevel
|
||||
default:
|
||||
return zapcore.InfoLevel
|
||||
}
|
||||
}
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
package main
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * 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"
|
||||
"embed"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/handler/admin"
|
||||
"geekai/log"
|
||||
"geekai/service"
|
||||
"geekai/service/image"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/moderation"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/payment"
|
||||
"geekai/service/ppt"
|
||||
"geekai/service/sms"
|
||||
"geekai/service/sora"
|
||||
"geekai/service/suno"
|
||||
"geekai/service/video"
|
||||
"geekai/store"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = log.GetLogger()
|
||||
|
||||
//go:embed res
|
||||
var xdbFS embed.FS
|
||||
|
||||
// AppLifecycle 应用程序生命周期
|
||||
type AppLifecycle struct {
|
||||
}
|
||||
|
||||
// OnStart 应用程序启动时执行
|
||||
func (l *AppLifecycle) OnStart(context.Context) error {
|
||||
logger.Info("AppLifecycle OnStart")
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop 应用程序停止时执行
|
||||
func (l *AppLifecycle) OnStop(context.Context) error {
|
||||
logger.Info("AppLifecycle OnStop")
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAppLifeCycle() *AppLifecycle {
|
||||
return &AppLifecycle{}
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := os.Getenv("CONFIG_FILE")
|
||||
if configFile == "" {
|
||||
configFile = "config.toml"
|
||||
}
|
||||
logger.Info("Loading config file: ", configFile)
|
||||
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 {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
config.Path = configFile
|
||||
return config
|
||||
}),
|
||||
// 创建应用服务
|
||||
fx.Provide(core.NewServer),
|
||||
// 初始化
|
||||
fx.Invoke(func(s *core.AppServer, client *redis.Client) {
|
||||
s.Init(client)
|
||||
}),
|
||||
fx.Provide(func(db *gorm.DB) *types.SystemConfig {
|
||||
return core.LoadSystemConfig(db)
|
||||
}),
|
||||
|
||||
// 初始化数据库
|
||||
fx.Provide(store.NewGormConfig),
|
||||
fx.Provide(store.NewMysql),
|
||||
fx.Provide(store.NewRedisClient),
|
||||
fx.Provide(store.NewLevelDB),
|
||||
|
||||
fx.Provide(func() embed.FS {
|
||||
return xdbFS
|
||||
}),
|
||||
|
||||
// 创建 Ip2Region 查询对象
|
||||
fx.Provide(func() (*xdb.Searcher, error) {
|
||||
file, err := xdbFS.Open("res/ip2region.xdb")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cBuff, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return xdb.NewWithBuffer(cBuff)
|
||||
}),
|
||||
|
||||
// 创建控制器
|
||||
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.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),
|
||||
fx.Provide(admin.NewUserHandler),
|
||||
fx.Provide(admin.NewChatAppHandler),
|
||||
fx.Provide(admin.NewRedeemHandler),
|
||||
fx.Provide(admin.NewDashboardHandler),
|
||||
fx.Provide(admin.NewChatModelHandler),
|
||||
fx.Provide(admin.NewProductHandler),
|
||||
fx.Provide(admin.NewOrderHandler),
|
||||
fx.Provide(admin.NewPowerLogHandler),
|
||||
fx.Provide(admin.NewAdminJimengHandler),
|
||||
fx.Provide(admin.NewVideoHandler),
|
||||
fx.Provide(admin.NewSunoHandler),
|
||||
fx.Provide(admin.NewPPTHandler),
|
||||
|
||||
// 邮件服务
|
||||
fx.Provide(service.NewSmtpService),
|
||||
// Image 服务
|
||||
fx.Provide(image.NewService),
|
||||
fx.Invoke(func(s *image.Service) {
|
||||
s.Run()
|
||||
s.DownloadImages()
|
||||
s.CheckTaskStatus()
|
||||
}),
|
||||
|
||||
// MidJourney service pool
|
||||
fx.Provide(mj.NewService),
|
||||
fx.Provide(mj.NewClient),
|
||||
fx.Invoke(func(s *mj.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.DownloadImages()
|
||||
}),
|
||||
|
||||
// Sora service
|
||||
fx.Provide(sora.NewSoraService),
|
||||
|
||||
fx.Provide(suno.NewService),
|
||||
fx.Invoke(func(s *suno.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
fx.Provide(video.NewService),
|
||||
fx.Invoke(func(s *video.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
|
||||
// 即梦AI 服务
|
||||
fx.Provide(jimeng.NewClient),
|
||||
fx.Provide(jimeng.NewService),
|
||||
fx.Invoke(func(service *jimeng.Service) {
|
||||
service.Start()
|
||||
}),
|
||||
|
||||
fx.Provide(service.NewSnowflake),
|
||||
|
||||
// 创建短信服务
|
||||
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.ChatAppHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PPTTaskHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 管理后台路由注册
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||
h.RegisterRoutes()
|
||||
h.StartSyncOrders()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewInviteHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(admin.NewFunctionHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(admin.NewUploadHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewFunctionHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewChatHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.PowerLogHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewMenuHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewMenuHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewMarkMapHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
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) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewVideoHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
|
||||
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) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewChatAppTypeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppTypeHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewTestHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewPromptHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
|
||||
// 微信公众号路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.WxGzhHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||
go func() {
|
||||
err := s.Run(db)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
}),
|
||||
fx.Provide(NewAppLifeCycle),
|
||||
// 注册生命周期回调函数
|
||||
fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) {
|
||||
lifecycle.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return lc.OnStart(ctx)
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return lc.OnStop(ctx)
|
||||
},
|
||||
})
|
||||
}),
|
||||
fx.Provide(admin.NewImageHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(handler.NewRealtimeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
)
|
||||
// 启动应用程序
|
||||
go func() {
|
||||
if err := app.Start(context.Background()); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 监听退出信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
// 关闭应用程序
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := app.Stop(ctx); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user