Compare commits
1172 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 31fbf7e48a | |||
| e2e24078c5 | |||
| 39ef92e1ce | |||
| f63ba187dd | |||
| 23d1ac1021 | |||
| f08329eef4 | |||
| e993a582cd | |||
| 1524e189dc | |||
| 2b5165324c | |||
| 8f67826072 | |||
| ac9a31f049 | |||
| ed2cdbbc31 | |||
| 8bd2feaf7e | |||
| 4eccfe6d2c | |||
| 5ba2e000a1 | |||
| fe9adadb3a | |||
| 805c2a045e | |||
| fd7b196134 | |||
| 1b1c327c35 | |||
| a228f603d2 | |||
| 7d0a05ee11 | |||
| 5f0a62b63e | |||
| 825a1b1027 | |||
| 38dde1a373 | |||
| 950e7d1b00 | |||
| 89ded74345 | |||
| d8f9f48278 | |||
| 818bb38617 | |||
| bdd76addf3 | |||
| 0202d75ff4 | |||
| 2936f21f12 | |||
| a0831ec6cd | |||
| 705ca4d20a | |||
| 2926aab7a4 | |||
| 6a066f1b8e | |||
| 775fb1f853 | |||
| b7d137247a | |||
| 22278c40bf | |||
| 4373642ebd | |||
| e29b32bd2d | |||
| 5bf90920f5 | |||
| fa6bb5e46a | |||
| edcbb3e226 | |||
| cebf8497a4 | |||
| 5213bdf08b | |||
| cded4bdcc7 | |||
| cf817fd8ea | |||
| 596b700d63 | |||
| a2481ff1cf | |||
| 59dfa95bf8 | |||
| bc7d06d3e5 | |||
| f6b5a94b29 | |||
| 8e81dfa12a | |||
| 188fb23f08 | |||
| 0ff76f0f21 | |||
| 5347a12035 | |||
| 787caa84c8 | |||
| 691d453f41 | |||
| c2503e663a | |||
| 5b7f2603ae | |||
| 405a88862b | |||
| 3fd7f810f0 | |||
| 296eabe09a | |||
| e04a48623d | |||
| 2e1bad387c | |||
| 54b45ec2ff | |||
| 5c18a50330 | |||
| c434f85045 | |||
| d78d3ffe02 | |||
| 4d10279870 | |||
| 44240f65d5 | |||
| 97e81a7dcc | |||
| 9e8f1ed6bf | |||
| 4dbfdab50d | |||
| c39814ce2b | |||
| 95a071014c | |||
| 6e03f4b363 | |||
| 9de9489673 | |||
| bb8644dea0 | |||
| ff9142ddd4 | |||
| 9814fec930 | |||
| 35f469fb82 | |||
| 53ba731159 | |||
| 705ad58a8f | |||
| c96f86fbbf | |||
| 4b3b64e9e2 | |||
| 2f2e146951 | |||
| a09c529414 | |||
| a8d1d58e95 | |||
| 6695be815e | |||
| fbd3478772 | |||
| 2102e1afbb | |||
| 155c56f502 | |||
| 37a9b0e485 | |||
| 52a3f13f1e | |||
| a678a11c33 | |||
| d34b785238 | |||
| 1086e1d631 | |||
| b2f57aa483 | |||
| 3094b9c8fd | |||
| 4c2dba1004 | |||
| 0f0a4c0a7e | |||
| c374126f69 | |||
| 8498cd71dc | |||
| bf30517393 | |||
| 495f86e7fc | |||
| 471017657f | |||
| 4861dd75be | |||
| 342ceea371 | |||
| 880c34dfee | |||
| a1e487100d | |||
| 77948b1e16 | |||
| e28a12a1ee | |||
| 00a8bc6784 | |||
| 8fffa60569 | |||
| 2debe7e927 | |||
| 478bc32ddd | |||
| dfd2be1265 | |||
| 33ff2e29e5 | |||
| 4df1c7a136 | |||
| c522248d39 | |||
| 246d96ee63 | |||
| 909ae4aa00 | |||
| 26e3ababcf | |||
| 4d9f89f630 | |||
| b989199edb | |||
| 0d81fe6c8e | |||
| be45f41e34 | |||
| e9ac58b1ef | |||
| 59d9ae96ac | |||
| 3cff6f7189 | |||
| 14aee28289 | |||
| 8c1b4d4516 | |||
| 068df8fa15 | |||
| e371310d02 | |||
| 96b8121210 | |||
| 1960a85ead | |||
| 69cb479e01 | |||
| db90131bd6 | |||
| c18d272413 | |||
| cd0952e170 | |||
| e6a6d0485d | |||
| a642ae0332 | |||
| 67f508c7da | |||
| 2225f09797 | |||
| fe7fa46a0c | |||
| 6d8901fa21 | |||
| 7750cb0021 | |||
| 327d68a347 | |||
| 7c272b0efd | |||
| 5eb4ed157e | |||
| d4e00b960c | |||
| 79adc871ef | |||
| af0eff88b8 | |||
| c755befa28 | |||
| 3f6890da27 | |||
| 8144fada25 | |||
| 08319391ea | |||
| ae7f1c03e2 | |||
| 151017ed47 | |||
| 941d15b635 | |||
| d8058400dd | |||
| 5a1ed953de | |||
| 58d127b602 | |||
| 13bf73e6cf | |||
| aa46fdb113 | |||
| 4a09da3a25 | |||
| d70fcbb9f3 | |||
| a16ef6476d | |||
| dae75343d0 | |||
| d7564127d2 | |||
| c6bf54fd9d | |||
| 8d47072e1c | |||
| d601226187 | |||
| 4801832d9d | |||
| 6b0f42d0b8 | |||
| 08379c21ac | |||
| 2499bd9ad3 | |||
| ad6da0d320 | |||
| a2583c0591 | |||
| 0ea4a29152 | |||
| 7fe50788a5 | |||
| c4a68076d6 | |||
| 9e365895af | |||
| 9ece25e17d | |||
| 2140eff51b | |||
| dfba3c30a5 | |||
| 4b717109d2 | |||
| e54e908fbc | |||
| 7a11a9ef15 | |||
| c9d0700fd9 | |||
| f9b809801d | |||
| cc551ba266 | |||
| 754ba02263 | |||
| 23787ff462 | |||
| 7ddf57ae06 | |||
| 9f603c5e77 | |||
| 3d720bdd81 | |||
| cc5180a6f7 | |||
| a8eccbe43b | |||
| 2ac77ac39f | |||
| 4eaa518cf3 | |||
| 5622f94fc8 | |||
| 95457b7dcd | |||
| 6a9de72c78 | |||
| 088a614160 | |||
| 013ee98f53 | |||
| dc46ed0e05 | |||
| 2a0c657ca3 | |||
| a0aee80c63 | |||
| 4910be3403 | |||
| 1541d74c84 | |||
| 34c9151dc1 | |||
| 6c69770ed6 | |||
| 5b7c38c67f | |||
| b7bec8ecb7 | |||
| 82fe9c3596 | |||
| 9f44c34d34 | |||
| afd84516b0 | |||
| 024f00b73c | |||
| 24a21bf2ee | |||
| f22c6bf658 | |||
| 0df700ec18 | |||
| 46141f87b8 | |||
| eecce10018 | |||
| b793b81768 | |||
| c8506e3d3b | |||
| bddd611cc1 | |||
| b399fc557a | |||
| 233f6e00f0 | |||
| b20ea734fd | |||
| b7dba68549 | |||
| 7ab0acd6a4 | |||
| 1d60ccc516 | |||
| 2a8d2213b1 | |||
| a27ce36a32 | |||
| 3fdcc895ed | |||
| 88e510d07a | |||
| 2526feb0d9 | |||
| 4171b2bdfb | |||
| 0611f8de6c | |||
| df9e495dc5 | |||
| 8a48476105 | |||
| 9d4a1705a5 | |||
| 871e5733c7 | |||
| bdea12c51a | |||
| e8a663e3c7 | |||
| a27d9ea259 | |||
| 422d439627 | |||
| 7cd824c284 | |||
| edeff3c169 | |||
| e27d95e2b5 | |||
| 13076421a1 | |||
| 3943e7f05f | |||
| 6839827db0 | |||
| cd0d9dad98 | |||
| 857da34b9c | |||
| ccad7e7bb5 | |||
| b8de15d66e | |||
| c02661ea29 | |||
| 3c70c8ae59 | |||
| d6a04f96fe | |||
| b985b62068 | |||
| 297b760293 | |||
| 5f820b9dc1 | |||
| 088abfe7ab | |||
| 17340ae0ac | |||
| fe687af8ca | |||
| 7e26029268 | |||
| 03f7f2a53e | |||
| 296260bf6a | |||
| dd07acd21a | |||
| 89f6402fbf | |||
| 6788edbe9d | |||
| 853059c814 | |||
| 3895305882 | |||
| 5a0876f8dc | |||
| 2e32238652 | |||
| c2acbaaa94 | |||
| ad1a99aa44 | |||
| 24e4be019a | |||
| 13c917ad7e | |||
| 0a8cf6870f | |||
| 061291cebb | |||
| b8e0d7760b | |||
| 962de0183c | |||
| 627396dbf7 | |||
| 6c300bb018 | |||
| 9273df4af2 | |||
| 4a99be2f15 | |||
| 669d784f54 | |||
| 5253d657b6 | |||
| 27c816cf3b | |||
| c2548c5007 | |||
| 0d81776212 | |||
| bdec823148 | |||
| a6de958daa | |||
| cccab31c0f | |||
| 50c25d4574 | |||
| 68100f7f24 | |||
| 38777ea285 | |||
| 00c8f08179 | |||
| 45d6579fb6 | |||
| dad9254128 | |||
| 4ddf3bf2bf | |||
| 9934143f00 | |||
| f617bde81b | |||
| b2771b7b3f | |||
| 44b3237dd6 | |||
| b987a2d365 | |||
| 1cf299f090 | |||
| c87d78c666 | |||
| 3d37a3d367 | |||
| 79c8d90049 | |||
| 73d8236697 | |||
| 1afc3e5e1c | |||
| 87e8bbfa41 | |||
| 3e3d3e162e | |||
| cc80b87fee | |||
| b614ecccc2 | |||
| 8251c6589b | |||
| 4edf2ea02a | |||
| 12dd4659cd | |||
| 5d4f40c004 | |||
| 3c34e8e0e7 | |||
| 7441a3a717 | |||
| 3eef9a836b | |||
| 922202734a | |||
| fffd7e375f | |||
| d87f09c957 | |||
| b29884f8df | |||
| b270960a04 | |||
| c0ee2c2d8e | |||
| 5c4899df6e | |||
| be1580e949 | |||
| 6424eb871c | |||
| 8a8c43c7a5 | |||
| 9a503ddc72 | |||
| b130466c8f | |||
| 2f0215ac87 | |||
| 3ab5459778 | |||
| dd5cc206e5 | |||
| 4eb1bf2de2 | |||
| 6c47551b03 | |||
| 657ecccee3 | |||
| b4ba70c0f3 | |||
| c2bf5e845a | |||
| 8900e72e45 | |||
| 0cb192135d | |||
| c1ca687630 | |||
| 29cc24508b | |||
| 3db55cbd48 | |||
| c0a7f41747 | |||
| 1ba08bbfa6 | |||
| 51a8f42d89 | |||
| 3b081ff0f4 | |||
| 7f31a301e3 | |||
| 039d949523 | |||
| 5b16dc6ee7 | |||
| 34648c704f | |||
| 1966e69460 | |||
| 74b1b07b31 | |||
| 2e023cb8dc | |||
| 118c0e1ff1 | |||
| e933f32d9c | |||
| 81d075c028 | |||
| 514dd6c76a | |||
| 0b2501c1d8 | |||
| ddc323a8c3 | |||
| 2933c057a2 | |||
| c1d892069e | |||
| 20ed6d1d3d | |||
| 90f5275201 | |||
| fe64b203da | |||
| 9eb1353455 | |||
| 8a1c55c731 | |||
| 9059bebd07 | |||
| d18f79fe74 | |||
| 3a8f0be28a | |||
| 60cf380f96 | |||
| ab8240613e | |||
| d6e9dc6839 | |||
| 6891bdde53 | |||
| bda335212d | |||
| ecadfeab19 | |||
| 06f4cdc649 | |||
| a38d547200 | |||
| a15f431a7f | |||
| 3bb5b66e8b | |||
| 9bf7fa4081 | |||
| d9ff3242c6 | |||
| cd562ab8ed | |||
| 3a8a69ac2e | |||
| 4ca9dfd9c0 | |||
| 0d5b4936bd | |||
| b5f6eaf159 | |||
| adfee8bf58 | |||
| 25c80921e0 | |||
| fbfa2a71a9 | |||
| 5c51ed48c3 | |||
| 22febfc42a | |||
| de482dd9f4 | |||
| 74af67a41f | |||
| 170b41441b | |||
| 4991e50e16 | |||
| 1f236ef929 | |||
| 7f86c5984d | |||
| 6fcee49b19 | |||
| ac527851a0 | |||
| 64bbeaedcb | |||
| 3e984a1bcb | |||
| a7237fe62f | |||
| 79165214d7 | |||
| c3c454b7d7 | |||
| 3f7855dd6e | |||
| d4d708d44b | |||
| f0610281e5 | |||
| 7f0b6a3a46 | |||
| e87e7e2164 | |||
| c2a7c089d2 | |||
| 1a3ad390fd | |||
| df5bd4df60 | |||
| c383367e9f | |||
| 79b6010104 | |||
| 7a9f45c96b | |||
| 97b0a98793 | |||
| 7277cb289f | |||
| 86b2d5eff2 | |||
| 7873847616 | |||
| 5e46f149d9 | |||
| 574fc52332 | |||
| 8c924ca98f | |||
| a9adc0c1a2 | |||
| ed286c8894 | |||
| d900a3d08e | |||
| a55da15202 | |||
| cdf5b66729 | |||
| 9728118f94 | |||
| 9cc2ea412b | |||
| 3f1ad4b7dc | |||
| 56c225bf20 | |||
| 8b68bfb28c | |||
| a5299b52d4 | |||
| 9e9bc52737 | |||
| 76106e4504 | |||
| a6c00c42fa | |||
| 4e23005b5b | |||
| f27e2de220 | |||
| eefcabcff3 | |||
| 63e4669e3f | |||
| 7d5428f775 | |||
| ab3c4562fa | |||
| 13f89bf335 | |||
| e6a18445c3 | |||
| c8ab209426 | |||
| 360fea4085 | |||
| 9794d67eaa | |||
| b60a639312 | |||
| 870706c4ff | |||
| 491471fa10 | |||
| 6dbf61d4e4 | |||
| 81545d192b | |||
| 37cdc26160 | |||
| fb6a35ebe4 | |||
| f7a565bb80 | |||
| bf5e72b7e0 | |||
| ec6186596d | |||
| dece19cec6 | |||
| 6aa1d27711 | |||
| 264e77f383 | |||
| 0cbc284e42 | |||
| 9e42a334fa | |||
| 49438d2ee1 | |||
| 735aa24020 | |||
| 14f63a203d | |||
| e11c0a3633 | |||
| ca83f139f7 | |||
| 58e44b7d12 | |||
| 8f47474edd | |||
| c5200aada8 | |||
| 4e39b93673 | |||
| f215643f2e | |||
| aa72be6ff3 | |||
| b567e56b60 | |||
| 17431c1707 | |||
| 7c9ebef5e9 | |||
| 72b0e11543 | |||
| 9e2af9dbca | |||
| b7cc987132 | |||
| 8476c73b0c | |||
| e76b49afd2 | |||
| c712b95e0d | |||
| 1ee1a8d6d9 | |||
| f01c764589 | |||
| 6df66d150b | |||
| 6132f94eff | |||
| ac5e67ea73 | |||
| 143d2b44d0 | |||
| ef130fe377 | |||
| d7d1e2100c | |||
| af5afd7700 | |||
| a64df5cf0c | |||
| 3866319373 | |||
| ec5ad809f2 | |||
| 268acb174d | |||
| 3733877be1 | |||
| 6d5579a8d6 | |||
| 306cd2f945 | |||
| 5d4dd1e66f | |||
| 5f371bdc4a | |||
| aa58ba392e | |||
| 0485610818 | |||
| 7cb8becc09 | |||
| 8fa535a01b | |||
| 5f202e03a2 | |||
| ef2dfe330b | |||
| fcd86fbebd | |||
| 960d294aa2 | |||
| 27f8e63f3d | |||
| e8d7ad58be | |||
| f4d537e0f5 | |||
| d6c93dd537 | |||
| aa5fff47de | |||
| c920d8423d | |||
| b8fcfe2e7c | |||
| 072c96e1ec | |||
| 38c8ece642 | |||
| 9bf1576fb4 | |||
| d648e177c4 | |||
| e8f62af7f6 | |||
| e746aafa2f | |||
| 71f7a1b166 | |||
| 678f570a90 | |||
| 4bc1f195db | |||
| b2ff49ee94 | |||
| d9558f13ad | |||
| 8dc8f2567e | |||
| 316636f83c | |||
| e3c2fbf82a | |||
| 1b6175e598 | |||
| 26dc479596 | |||
| dca25dbb78 | |||
| c55275872e | |||
| d4d1fb26f6 | |||
| a48cb7bc34 | |||
| 8af6e9290f | |||
| 14b277d813 | |||
| 20915038a3 | |||
| 15723457cb | |||
| be8a0ec184 | |||
| ec79fc933c | |||
| b02e3aad95 | |||
| 3ed5a420a7 | |||
| 08eca511ad | |||
| 711e701933 | |||
| c34e911596 | |||
| e6bbd83c6b | |||
| 8a452c3072 | |||
| b99b310306 | |||
| 13bfb14107 | |||
| e3c46016e0 | |||
| 4188b0969e | |||
| eb739fcccb | |||
| 0c27795a10 | |||
| 9a94505f43 | |||
| d05693c5c1 | |||
| 7c5c3d8a3c | |||
| c0b2063b38 | |||
| 7027f9a55b | |||
| 4d183747b1 | |||
| cf3d8af260 | |||
| 08fe1b2f75 | |||
| 4ea7352674 | |||
| db3e8a267e | |||
| 6e6885849b | |||
| 8fc62682c4 | |||
| 827f2b9739 | |||
| 75031914a3 | |||
| 54d70623ab | |||
| a4c9fdd95a | |||
| ed819e8c79 | |||
| 6a9bfeb5aa | |||
| 94680c4411 | |||
| e654766f60 | |||
| 8bcbb68438 | |||
| 0ef6955f96 | |||
| b9f31d42f4 | |||
| b4501557c9 | |||
| dc2b768c6b | |||
| a2ed99e6cb | |||
| 60df732db7 | |||
| 6bd6bb3885 | |||
| 6d85d128cc | |||
| 399cf65fc9 | |||
| 96785ca037 | |||
| 24906a6df1 | |||
| 7cfc747653 | |||
| d772bbebe6 | |||
| 05f911628a | |||
| 14988853a3 | |||
| 18b718a328 | |||
| 7b3f16ac9f | |||
| 6d9b9bd3f7 | |||
| 82b2755c18 | |||
| 04e990aeb3 | |||
| cbbcd4c571 | |||
| 5ad9adeb4a | |||
| 13c9594be0 | |||
| e4f5f78bfa | |||
| bfa0208692 | |||
| 64994cdeda | |||
| 9796a841d9 | |||
| 404b493d23 | |||
| c04f9eb993 | |||
| 825da42208 | |||
| 721a8f1ecb | |||
| 586a174d6c | |||
| bec6966542 | |||
| b1f4e1d4d8 | |||
| d7d64e8259 | |||
| 08b498fccd | |||
| ecc5a52232 | |||
| 7327b90255 | |||
| 6ce7a9c16f | |||
| eb2658a91c | |||
| 9f59a18756 | |||
| ef06f0da98 | |||
| 4dec0e2c69 | |||
| 5f36f1af11 | |||
| 5a1a596098 | |||
| 7a25c6e062 | |||
| a73ed71b9d | |||
| 2abbb95ea2 | |||
| 1f80ba9463 | |||
| 4327fdac12 | |||
| a6f05a5874 | |||
| 3fb74a1598 | |||
| 2ac44cdeb6 | |||
| 4e4dc4cb73 | |||
| 855a953010 | |||
| 3264fda06e | |||
| 2c7d472069 | |||
| 8182e6797f | |||
| 99cb48ce88 | |||
| e391bfca80 | |||
| 0181ad3715 | |||
| b95dff0751 | |||
| 668d4c9c64 | |||
| 66c0d1b2f7 | |||
| bef0996375 | |||
| 391a1e4cf0 | |||
| 38facf517a | |||
| 5c9b1e8764 | |||
| dbcf14b566 | |||
| cc59f0c761 | |||
| f263fc067a | |||
| 6635b88baa | |||
| 87ea2a611f | |||
| ea8a2135cd | |||
| 76d86395ef | |||
| e268870636 | |||
| 49e9f41ef2 | |||
| 68cda968a1 | |||
| f6826fcefc | |||
| 56a91db7e7 | |||
| 8f4d94f046 | |||
| 9446bf5f98 | |||
| 23e353b18b | |||
| 46daaa4ba1 | |||
| f66d59531d | |||
| b76cd1d5ae | |||
| 55ba0d08a5 | |||
| cc78780a8e | |||
| 4a00809061 | |||
| cd760bbd84 | |||
| 84bdc6be7f | |||
| 8dd6bf8933 | |||
| 08f170d217 | |||
| 20da6a1c20 | |||
| 8586931630 | |||
| 667fc79a6f | |||
| ac7430dded | |||
| 9c73e16560 | |||
| c605e222a7 | |||
| 578f26a738 | |||
| 24218e7570 | |||
| 16f946550e | |||
| 9960a31485 | |||
| 437632f859 | |||
| 98f0f442eb | |||
| 3b999dc5e2 | |||
| 20943ad3d3 | |||
| 3b1544b5e4 | |||
| b317d597ac | |||
| 3bb19811e7 | |||
| 065a01de48 | |||
| 09fb2f6557 | |||
| 9ca97bf4bc | |||
| 0c59503edb | |||
| 6fa08861f8 | |||
| 72b95151a1 | |||
| 378e6ec9af | |||
| 27b68c1174 | |||
| 1ae8eabe99 | |||
| 72726c1cf9 | |||
| 6203d9a150 | |||
| 861f11af66 | |||
| 39fdfae541 | |||
| 9f57fb1421 | |||
| 059f57db2d | |||
| 13c8be8d06 | |||
| e0dbfb0488 | |||
| 722ed128a1 | |||
| cc628821e6 | |||
| 857af61af1 | |||
| f6fc484cb3 | |||
| ba07946f1e | |||
| 600f668c63 | |||
| 8a8990b12b | |||
| fe85c1bbb9 | |||
| a89ff72308 | |||
| a99e58184b | |||
| d981bc8fd0 | |||
| ff22480d89 | |||
| 8ec85e2829 | |||
| 706aacd8e6 | |||
| 803bc2d9fc | |||
| 7cc67cf8f0 | |||
| 9909c3a0ed | |||
| 60a3751839 | |||
| ce87e0c40b | |||
| 71cd548c00 | |||
| 1ddd05302b | |||
| ea3301c75c | |||
| 4b1c4f7ccc | |||
| 21f2622a4b | |||
| 79f6a43019 | |||
| 3d75093b2c | |||
| 023dc89c3e | |||
| b5641be30d | |||
| ff4515bbf1 | |||
| 5dc0fe05af | |||
| 25bba912f6 | |||
| cb6e323596 | |||
| cf4b04e047 | |||
| 243b5be31c | |||
| c3d753cf38 | |||
| 70c46d098b | |||
| 816e4a096e | |||
| 1d1e819817 | |||
| 342daf1be8 | |||
| f6bb9b582a | |||
| aad62bc976 | |||
| 38a49a7f37 | |||
| f603bf6be7 | |||
| 6cc05e9e27 | |||
| c00e67542f | |||
| 22660362a3 | |||
| b1ab9975b7 | |||
| 7ca89d8b54 | |||
| 998b07ab8c | |||
| 4936896ff7 | |||
| 18b7484c5b | |||
| 6754c8e85e | |||
| 57243ff010 | |||
| 7c4dfe96ee | |||
| bf19120c27 | |||
| 3d37087916 | |||
| 6949f1328c | |||
| b63090c20f | |||
| b15ebda948 | |||
| 252eef2e5e | |||
| 3bea8f9706 | |||
| dfe808bee7 | |||
| 58f3dd5336 | |||
| b00f16648f | |||
| fc66c31e89 | |||
| a26c849532 | |||
| d2991e60b6 | |||
| 10ba1430f9 | |||
| 6d71f24f75 | |||
| 8f4d20e411 | |||
| cf758d773e | |||
| c012f0c4c5 | |||
| 9e85900057 | |||
| 68e3b787f9 | |||
| 6706704ad6 | |||
| 1067fa015e | |||
| 411335ebcb | |||
| f19c15491e | |||
| 0da81a4d10 | |||
| 049839a5c7 | |||
| a901927844 | |||
| 07ab0bc5a1 | |||
| a6025e6fab | |||
| e841a61bf0 | |||
| 2f1c2110c6 | |||
| 8e573eea4c | |||
| 149765764e | |||
| 45d6444c7e | |||
| aa65f49190 | |||
| 1bda41ff67 | |||
| 5a799139cd | |||
| eb36d0742a | |||
| d8a9123852 | |||
| 19031c2197 | |||
| 7a2ffdf39c | |||
| ce8fa79206 | |||
| b05c63549d | |||
| 9f98660423 | |||
| 7b0fa862d2 | |||
| bfe2d4d573 | |||
| 26950c5673 | |||
| 86788362a5 | |||
| deedc5fb2e | |||
| f4fbe67db9 | |||
| 599ce0eade | |||
| 11f3ab8dc7 | |||
| 75b4a6dd46 | |||
| 86bbea337d | |||
| e63a30064b | |||
| 222b1ddbd9 | |||
| a88dd88a07 | |||
| a47c01fd41 | |||
| 35e385f7a7 | |||
| 191e3b7d2c | |||
| 13fbbc190a | |||
| 61c6a6e7f3 | |||
| 911f218f0b | |||
| af41350eb9 | |||
| 8f63e348b3 | |||
| 8901cf8e81 | |||
| e11736e1d2 | |||
| c062fba69c | |||
| 4628095fe7 | |||
| 787ed9bc0f | |||
| daaff8974e | |||
| 4fcbd511c0 | |||
| 64d6bff6d9 | |||
| 998cbace92 | |||
| 2204038951 | |||
| 998f67ac5d | |||
| be981a2c9b | |||
| da3cf0d4a3 | |||
| 7124b9a36c | |||
| 712a6b6c39 | |||
| ed0a1c57d6 | |||
| 7858228505 | |||
| 2525a22d78 | |||
| 7b4ef8fc31 | |||
| ce14cd02e4 | |||
| 05f501af52 | |||
| e94fa4844f | |||
| e0965aae5e | |||
| 414c1de963 | |||
| b589102be8 | |||
| df367e0d47 | |||
| 680219ebcb | |||
| ef87487f60 | |||
| 8bafe72434 | |||
| 76dcc69e44 | |||
| 682ef22194 | |||
| ee8dd41605 | |||
| d0b8d666e4 | |||
| 4a81826d19 | |||
| 848a10c3a8 | |||
| a4baa29678 | |||
| a9b5280691 | |||
| 264706210b | |||
| 7677ae254f | |||
| 7ccb4c5f06 | |||
| 01205af018 | |||
| 5b5150e6d4 | |||
| 9d8e3f5049 | |||
| 99ede83bdc | |||
| 61488e840d | |||
| a80d01209c | |||
| d2a8d655c8 | |||
| 80e2b34abc | |||
| fb3d43d2d5 | |||
| ebdec4fa44 | |||
| ead8dbbaa5 | |||
| 95615a5501 | |||
| 75487b1f58 | |||
| c7daaa00cd | |||
| 606f24e981 | |||
| c83d9287a7 | |||
| fa8092b2cd | |||
| b2b07a2be6 | |||
| a21628e350 | |||
| 9fcd686fda | |||
| 1759fd4cf9 | |||
| 12e7837602 | |||
| ff349f2edf | |||
| 088d8ca207 | |||
| c37902d3a4 | |||
| da38684cdd | |||
| cb2cb72333 | |||
| d53c8f9830 | |||
| c8136d4231 | |||
| e4b8380530 | |||
| fbd599194c | |||
| 0270ec26fb | |||
| 79e342be57 | |||
| c89f23b018 | |||
| f971ec5d34 | |||
| c5776ce41f | |||
| 75c5ebbffa | |||
| d51a724ade | |||
| c1143d7a6d | |||
| f3697431a4 | |||
| e3d6e5f420 | |||
| af6f7a7146 | |||
| c18d95e89a | |||
| fa42d03a65 | |||
| 22f7de2c09 | |||
| b4b9df81cb | |||
| 2a71c2b0e7 | |||
| c37b3d3df5 | |||
| ae135b89d6 | |||
| 4112426848 | |||
| 638b399b31 | |||
| 8a5713ef7c | |||
| 382d7bfaa1 | |||
| 3daf2f7738 | |||
| 944d35971d | |||
| 9ce8af0b82 | |||
| d825b9ec26 | |||
| 9afe4aea4b | |||
| 3cc8c3284a | |||
| e2c18c4e1e | |||
| 81fe768f6a | |||
| 8923779ab0 | |||
| 583218a045 | |||
| 153b2bfa53 | |||
| 0a9b325360 | |||
| af9e6f2c46 | |||
| 0e31726fd3 | |||
| 590d9c2e61 | |||
| c12fe9f25e | |||
| 9bd83f3acf | |||
| aada15fc41 | |||
| 6a2d383be6 | |||
| 545c8fa482 | |||
| e3d18643b8 | |||
| 2f318bbe9f | |||
| fac7eb61b3 | |||
| 4450d03a54 | |||
| e4e7868b30 | |||
| 3d7fa680cb | |||
| c38035e25e | |||
| 8d4fdaf902 | |||
| 2820adad53 | |||
| f7a427d2c0 | |||
| 59ed8c9660 | |||
| 71562ab0e5 | |||
| 278ddf037f | |||
| ac3151af92 | |||
| 1189ad7862 | |||
| 88c13e7b9a | |||
| 596baac62e | |||
| 5a8503ec14 | |||
| e44f95724d | |||
| dec0d0dea7 | |||
| 46f96e94ec | |||
| 89b30bcf58 | |||
| 473d8f5f85 | |||
| 08d8d65599 | |||
| 2964c1d82e | |||
| 75ab8552d9 | |||
| b78d5b3d03 | |||
| 064081c771 | |||
| 3525d35d15 | |||
| 9d9ee7c585 | |||
| e65c32c4c7 | |||
| 113769791f | |||
| 5d2a1d21d5 | |||
| 6e40f92aaf | |||
| 302cb8a5be | |||
| 0b27890484 | |||
| 5db247e632 | |||
| c1155a4338 | |||
| da806b9492 | |||
| 3d6d46b30d | |||
| 57c69738ba | |||
| 0c8157dbc0 | |||
| d7b278f2f7 | |||
| 18d74f1057 | |||
| e04856f794 | |||
| d7bbfb0fc3 | |||
| a0857817de | |||
| 8d39234fd0 | |||
| 0fce1c0fd8 | |||
| c23c285f82 | |||
| 81b560d791 | |||
| 55459a4f42 | |||
| 9eafd3e6ca | |||
| e7ac26ff5a | |||
| fad78641d6 | |||
| a32cb0142b | |||
| 4a3c133152 | |||
| ae79c34508 | |||
| 7eb69c1ad7 | |||
| 8aaff0af89 | |||
| 4b77280c65 | |||
| a0df84e4f2 | |||
| 8876e6a098 | |||
| 9bee2a90f9 | |||
| cd6a811bbd | |||
| 84f9a83f55 | |||
| 2efc669ab2 | |||
| 5444ed77ad | |||
| dd71fe80a5 | |||
| b73092eb64 | |||
| aa28f87b0c | |||
| 1e1bcd4a30 | |||
| d24b3c46bf | |||
| 37222f07d9 | |||
| 49646a79e5 | |||
| 9940e1210e | |||
| 10892812c3 | |||
| d8ff5987dd | |||
| d014d418e9 | |||
| cc1b56501d | |||
| a5ad9648bf | |||
| 3630099234 | |||
| 941a24c75b | |||
| 50bdc01484 | |||
| d9e340ddc4 | |||
| beee1e91d6 | |||
| 35935d2bac | |||
| 5d0b0ad33e | |||
| d1a6ac531f | |||
| ae255a3bd9 | |||
| fa733afa51 | |||
| b60c723457 | |||
| 2bb7ff3c13 | |||
| 68b4cf00e2 | |||
| 440169ff60 | |||
| 1fc361242e | |||
| 72cc6f3d75 | |||
| a2b1924e00 | |||
| 7e926167f0 | |||
| 32484bd32e | |||
| 04ade1008b | |||
| 4603de0e70 | |||
| 12c1ff65e9 | |||
| 7b8cbf7f86 | |||
| c198e83f70 | |||
| e60863a290 | |||
| 946563d3b2 | |||
| 3f2ef1d54e | |||
| 0d2b60b905 | |||
| 161b11e428 | |||
| f7748d51df | |||
| 5d13b4b705 | |||
| 92e52a2284 | |||
| 3efd5fb77a | |||
| c3d62bb8d8 | |||
| c099e843d5 | |||
| 5f3a5871f1 | |||
| f01fdd0070 | |||
| d20cc367b8 | |||
| eadb9a733f | |||
| 32ac454b5b | |||
| 2cadd6af44 | |||
| 3c8b5cb313 | |||
| fcf2387794 | |||
| f9783b4806 | |||
| cbdc532f83 | |||
| 124554bae5 | |||
| 2725536d7d | |||
| beeef7a4f7 | |||
| 7f2ebb6aeb | |||
| c8e30383ba | |||
| a7d5a6ccb9 | |||
| be3380aaf3 | |||
| 40a4ab5410 | |||
| 54a2181960 | |||
| e490e15bc4 | |||
| 469844d97b | |||
| 892ea29ba8 | |||
| acde1c6742 | |||
| 07da11d852 | |||
| 502b8c2270 | |||
| af3f7ac810 | |||
| 935c6caf96 | |||
| 90bce1d437 | |||
| 831dd3e2e0 | |||
| b8e208eb32 | |||
| 542c908e30 | |||
| 08529242bf | |||
| 1ae9449d92 | |||
| 7fd0b1fa08 | |||
| 111572e3f2 | |||
| c9875d24b4 | |||
| 94a75603c3 | |||
| d55925bccd | |||
| 0b5adcbfab | |||
| 8d84562f32 | |||
| 136d159833 | |||
| e3026062d8 | |||
| 8a69c69b7f | |||
| 197714a57a | |||
| 567cdc6f8d | |||
| be6e8c7713 | |||
| f667b35403 | |||
| 942e482ca4 | |||
| 10b381965e | |||
| 79c3a99a38 | |||
| 463d3c8e97 | |||
| da007729c6 | |||
| b7b42b5fd4 | |||
| 735f9bd053 | |||
| 6096b65374 | |||
| 727d342eb3 | |||
| 58815ba8bc | |||
| 49d0502775 | |||
| 0b6c6e6d2c | |||
| 45ede6047a | |||
| 8f469d4ebb | |||
| f3264d056d |
@@ -0,0 +1,5 @@
|
|||||||
|
web/src/assets/fonts filter=lfs diff=lfs merge=lfs -text
|
||||||
|
web/src/assets/iconfont filter=lfs diff=lfs merge=lfs -text
|
||||||
|
web/src/assets/img filter=lfs diff=lfs merge=lfs -text
|
||||||
|
web/public filter=lfs diff=lfs merge=lfs -text
|
||||||
|
api/res filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -14,3 +14,4 @@ logs
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
miniprogram
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## v4.2.8
|
||||||
|
|
||||||
|
- Bug 修复:修复管理后台邮件配置报“参数错误”问题
|
||||||
|
- 功能优化:手机端也不显示后端禁用的菜单功能页面
|
||||||
|
- 功能优化:**聊天页面支持 Sora2 文生视频/图生视频功能** 🔥🔥🔥
|
||||||
|
- 功能优化:**重构路由代码,优化路由加载逻辑,支持关闭手机站功能** 🔥🔥🔥
|
||||||
|
- Bug 修复:修复用户算力扣减日志记录逻辑错误问题
|
||||||
|
- 功能优化:优化 404 页面样式,兼容 PC 端和移动端
|
||||||
|
- Bug 修复:ipv6 地址登录报错:Data too long for column 'last_login_ip'
|
||||||
|
- Bug 修复:移动端登录页面输入密码的时候会覆盖在确认按钮上层
|
||||||
|
- 功能优化:**移动端的 DALL-E 绘图页面支持上传参考图** 🔥🔥🔥
|
||||||
|
- 功能优化:优化 WebFooter 组件,如果配置了备案号,则显示备案号,否则不显示
|
||||||
|
- 功能优化:启动时自动同步 model 与数据表字段,缺列自动新建、多余列自动删除
|
||||||
|
|
||||||
|
## v4.2.7
|
||||||
|
|
||||||
|
- Bug 修复:修复超级管理员无法修改密码的 Bug
|
||||||
|
- Bug 修复:微信登录配置更新后,没有同步更新到系统配置
|
||||||
|
- 功能优化: 给 AI 对话 API 加上线程锁,确保同一个用户同时只有一个对话请求
|
||||||
|
- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能。🔥🔥🔥
|
||||||
|
- 功能新增:新增 AI 对话编辑功能,并优化了重新生成逻辑
|
||||||
|
|
||||||
## v4.2.6
|
## v4.2.6
|
||||||
|
|
||||||
- 功能重构:优化系统配置管理功能,把 OSS,支付,短信,邮件等配置全部迁移到管理后台,无需通过修改配置文档的方式修改 🎉🎉🎉
|
- 功能重构:优化系统配置管理功能,把 OSS,支付,短信,邮件等配置全部迁移到管理后台,无需通过修改配置文档的方式修改 🎉🎉🎉
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ tmp
|
|||||||
bin
|
bin
|
||||||
data
|
data
|
||||||
config.toml
|
config.toml
|
||||||
|
config.toml.*.bak
|
||||||
static/upload
|
static/upload
|
||||||
static/audio
|
static/audio
|
||||||
storage.json
|
storage.json
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Listen = "0.0.0.0:5678"
|
Listen = "0.0.0.0:5678"
|
||||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
MysqlDns = "root:12345678@tcp(localhost:3306)/geekai?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||||
StaticDir = "./static" # 静态资源的目录
|
StaticDir = "./static" # 静态资源的目录
|
||||||
StaticUrl = "/static" # 静态资源访问 URL
|
StaticUrl = "/static" # 静态资源访问 URL
|
||||||
TikaHost = "http://tika:9998"
|
TikaHost = "http://tika:9998"
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *AppServer) Init(client *redis.Client) {
|
func (s *AppServer) Init(client *redis.Client) {
|
||||||
|
s.Engine.Use(middleware.ThumbMiddleware())
|
||||||
s.Engine.Use(middleware.ParameterHandlerMiddleware())
|
s.Engine.Use(middleware.ParameterHandlerMiddleware())
|
||||||
s.Engine.Use(errorHandler)
|
s.Engine.Use(errorHandler)
|
||||||
// 添加静态资源访问
|
// 添加静态资源访问
|
||||||
s.Engine.Static("/static", s.Config.StaticDir)
|
s.Engine.Static("/static", s.Config.StaticDir)
|
||||||
s.Engine.Use(middleware.StaticMiddleware())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AppServer) Run(db *gorm.DB) error {
|
func (s *AppServer) Run(db *gorm.DB) error {
|
||||||
|
|||||||
@@ -85,15 +85,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
|
|||||||
logger.Error("load system config error: ", err)
|
logger.Error("load system config error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载许可证配置
|
|
||||||
var license types.License
|
|
||||||
sysConfig.Id = 0
|
|
||||||
db.Where("name", types.ConfigKeyLicense).First(&sysConfig)
|
|
||||||
err = utils.JsonDecode(sysConfig.Value, &license)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("load license config error: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载验证码配置
|
// 加载验证码配置
|
||||||
var captchaConfig types.CaptchaConfig
|
var captchaConfig types.CaptchaConfig
|
||||||
sysConfig.Id = 0
|
sysConfig.Id = 0
|
||||||
@@ -168,7 +159,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
|
|||||||
|
|
||||||
return &types.SystemConfig{
|
return &types.SystemConfig{
|
||||||
Base: baseConfig,
|
Base: baseConfig,
|
||||||
License: license,
|
|
||||||
SMS: smsConfig,
|
SMS: smsConfig,
|
||||||
OSS: ossConfig,
|
OSS: ossConfig,
|
||||||
SMTP: smtpConfig,
|
SMTP: smtpConfig,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
|
logger2 "geekai/logger"
|
||||||
"geekai/utils"
|
"geekai/utils"
|
||||||
"geekai/utils/resp"
|
"geekai/utils/resp"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,6 +14,8 @@ import (
|
|||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
// 前端用户授权验证
|
// 前端用户授权验证
|
||||||
func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
|
func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"golang.org/x/image/webp"
|
"golang.org/x/image/webp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 静态资源中间件
|
// 缩略图中间件
|
||||||
func StaticMiddleware() gin.HandlerFunc {
|
func ThumbMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|
||||||
url := c.Request.URL.String()
|
url := c.Request.URL.String()
|
||||||
@@ -37,22 +37,6 @@ type RedisConfig struct {
|
|||||||
DB int
|
DB int
|
||||||
}
|
}
|
||||||
|
|
||||||
// LicenseKey 存储许可证书的 KEY
|
|
||||||
const LicenseKey = "Geek-AI-License"
|
|
||||||
|
|
||||||
type License struct {
|
|
||||||
Key string `json:"key"` // 许可证书密钥
|
|
||||||
MachineId string `json:"machine_id"` // 机器码
|
|
||||||
ExpiredAt int64 `json:"expired_at"` // 过期时间
|
|
||||||
IsActive bool `json:"is_active"` // 是否激活
|
|
||||||
Configs LicenseConfig `json:"configs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LicenseConfig struct {
|
|
||||||
UserNum int `json:"user_num"` // 用户数量
|
|
||||||
DeCopy bool `json:"de_copy"` // 去版权
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c RedisConfig) Url() string {
|
func (c RedisConfig) Url() string {
|
||||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||||
}
|
}
|
||||||
@@ -89,6 +73,7 @@ type BaseConfig struct {
|
|||||||
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
||||||
|
|
||||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||||
|
IndexPage string `json:"index_page"` // 首页显示的页面
|
||||||
Copyright string `json:"copyright"` // 版权信息
|
Copyright string `json:"copyright"` // 版权信息
|
||||||
ICP string `json:"icp"` // ICP 备案号
|
ICP string `json:"icp"` // ICP 备案号
|
||||||
GaBeian string `json:"ga_beian"` // 公安备案号
|
GaBeian string `json:"ga_beian"` // 公安备案号
|
||||||
@@ -96,6 +81,8 @@ type BaseConfig struct {
|
|||||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||||
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
||||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||||
|
|
||||||
|
EnableMobileSite bool `json:"enable_mobile_site"` // 是否开启手机站点
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemConfig struct {
|
type SystemConfig struct {
|
||||||
@@ -107,7 +94,6 @@ type SystemConfig struct {
|
|||||||
Captcha CaptchaConfig
|
Captcha CaptchaConfig
|
||||||
WxLogin WxLoginConfig
|
WxLogin WxLoginConfig
|
||||||
Jimeng JimengConfig
|
Jimeng JimengConfig
|
||||||
License License
|
|
||||||
Moderation ModerationConfig
|
Moderation ModerationConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +106,6 @@ const (
|
|||||||
ConfigKeyMarkMap = "mark_map"
|
ConfigKeyMarkMap = "mark_map"
|
||||||
ConfigKeyCaptcha = "captcha"
|
ConfigKeyCaptcha = "captcha"
|
||||||
ConfigKeyWxLogin = "wx_login"
|
ConfigKeyWxLogin = "wx_login"
|
||||||
ConfigKeyLicense = "license"
|
|
||||||
ConfigKeySms = "sms"
|
ConfigKeySms = "sms"
|
||||||
ConfigKeySmtp = "smtp"
|
ConfigKeySmtp = "smtp"
|
||||||
ConfigKeyOss = "oss"
|
ConfigKeyOss = "oss"
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ func init() {
|
|||||||
|
|
||||||
// CaptchaConfig 行为验证码配置
|
// CaptchaConfig 行为验证码配置
|
||||||
type CaptchaConfig struct {
|
type CaptchaConfig struct {
|
||||||
ApiKey string `json:"api_key"`
|
ApiKey string `json:"api_key,omitempty"`
|
||||||
Type string `json:"type"` // 验证码类型, 可选值: "dot" 或 "slide"
|
Type string `json:"type,omitempty"` // 验证码类型, 可选值: "dot" 或 "slide"
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WxLoginConfig 微信登录配置
|
// WxLoginConfig 微信登录配置
|
||||||
type WxLoginConfig struct {
|
type WxLoginConfig struct {
|
||||||
ApiKey string `json:"api_key"`
|
ApiKey string `json:"api_key,omitempty"`
|
||||||
NotifyURL string `json:"notify_url"` // 登录成功回调 URL
|
NotifyURL string `json:"notify_url,omitempty"` // 登录成功回调 URL
|
||||||
Enabled bool `json:"enabled"` // 是否启用微信登录
|
Enabled bool `json:"enabled,omitempty"` // 是否启用微信登录
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,65 @@ package types
|
|||||||
|
|
||||||
// JimengConfig 即梦AI配置
|
// JimengConfig 即梦AI配置
|
||||||
type JimengConfig struct {
|
type JimengConfig struct {
|
||||||
AccessKey string `json:"access_key"`
|
// 即梦AI的AccessKey和SecretKey
|
||||||
SecretKey string `json:"secret_key"`
|
AccessKey string `json:"access_key,omitempty"`
|
||||||
Power JimengPower `json:"power"`
|
SecretKey string `json:"secret_key,omitempty"`
|
||||||
|
// 火山引擎大模型专用的验证方式
|
||||||
|
ApiKey string `json:"api_key,omitempty"`
|
||||||
|
// 算力配置
|
||||||
|
Powers map[string]int `json:"powers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JimengPower 即梦AI算力配置
|
// JMTaskStatus 任务状态
|
||||||
type JimengPower struct {
|
type JMTaskStatus string
|
||||||
TextToImage int `json:"text_to_image"`
|
|
||||||
ImageToImage int `json:"image_to_image"`
|
const (
|
||||||
ImageEdit int `json:"image_edit"`
|
JMTaskStatusSubmited = JMTaskStatus("submited") // 任务已提交
|
||||||
ImageEffects int `json:"image_effects"`
|
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务排队中
|
||||||
TextToVideo int `json:"text_to_video"`
|
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
|
||||||
ImageToVideo int `json:"image_to_video"`
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ package types
|
|||||||
|
|
||||||
// 文本审查
|
// 文本审查
|
||||||
type ModerationConfig struct {
|
type ModerationConfig struct {
|
||||||
Enable bool `json:"enable"` // 是否启用文本审查
|
Enable bool `json:"enable,omitempty"` // 是否启用文本审查
|
||||||
Active string `json:"active"`
|
Active string `json:"active,omitempty"`
|
||||||
EnableGuide bool `json:"enable_guide"` // 是否启用模型引导提示词
|
EnableGuide bool `json:"enable_guide,omitempty"` // 是否启用模型引导提示词
|
||||||
GuidePrompt string `json:"guide_prompt"` // 模型引导提示词
|
GuidePrompt string `json:"guide_prompt,omitempty"` // 模型引导提示词
|
||||||
Gitee ModerationGiteeConfig `json:"gitee"`
|
Gitee ModerationGiteeConfig `json:"gitee,omitempty"`
|
||||||
Baidu ModerationBaiduConfig `json:"baidu"`
|
Baidu ModerationBaiduConfig `json:"baidu,omitempty"`
|
||||||
Tencent ModerationTencentConfig `json:"tencent"`
|
Tencent ModerationTencentConfig `json:"tencent,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -26,26 +26,26 @@ const (
|
|||||||
|
|
||||||
// GiteeAI 文本审查配置
|
// GiteeAI 文本审查配置
|
||||||
type ModerationGiteeConfig struct {
|
type ModerationGiteeConfig struct {
|
||||||
ApiKey string `json:"api_key"`
|
ApiKey string `json:"api_key,omitempty"`
|
||||||
Model string `json:"model"` // 文本审核模型
|
Model string `json:"model,omitempty"` // 文本审核模型
|
||||||
}
|
}
|
||||||
|
|
||||||
// 百度文本审查配置
|
// 百度文本审查配置
|
||||||
type ModerationBaiduConfig struct {
|
type ModerationBaiduConfig struct {
|
||||||
AccessKey string `json:"access_key"`
|
AccessKey string `json:"access_key,omitempty"`
|
||||||
SecretKey string `json:"secret_key"`
|
SecretKey string `json:"secret_key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 腾讯云文本审查配置
|
// 腾讯云文本审查配置
|
||||||
type ModerationTencentConfig struct {
|
type ModerationTencentConfig struct {
|
||||||
AccessKey string `json:"access_key"`
|
AccessKey string `json:"access_key,omitempty"`
|
||||||
SecretKey string `json:"secret_key"`
|
SecretKey string `json:"secret_key,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModerationResult struct {
|
type ModerationResult struct {
|
||||||
Flagged bool `json:"flagged"`
|
Flagged bool `json:"flagged,omitempty"`
|
||||||
Categories map[string]bool `json:"categories"`
|
Categories map[string]bool `json:"categories,omitempty"`
|
||||||
CategoryScores map[string]float64 `json:"category_scores"`
|
CategoryScores map[string]float64 `json:"category_scores,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var ModerationCategories = map[string]string{
|
var ModerationCategories = map[string]string{
|
||||||
|
|||||||
@@ -8,39 +8,39 @@ package types
|
|||||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
type OSSConfig struct {
|
type OSSConfig struct {
|
||||||
Active string `json:"active"`
|
Active string `json:"active,omitempty"`
|
||||||
Local LocalStorageConfig `json:"local"`
|
Local LocalStorageConfig `json:"local,omitempty"`
|
||||||
Minio MiniOssConfig `json:"minio"`
|
Minio MiniOssConfig `json:"minio,omitempty"`
|
||||||
QiNiu QiNiuOssConfig `json:"qiniu"`
|
QiNiu QiNiuOssConfig `json:"qiniu,omitempty"`
|
||||||
AliYun AliYunOssConfig `json:"aliyun"`
|
AliYun AliYunOssConfig `json:"aliyun,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MiniOssConfig struct {
|
type MiniOssConfig struct {
|
||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
AccessKey string `json:"access_key"`
|
AccessKey string `json:"access_key,omitempty"`
|
||||||
AccessSecret string `json:"access_secret"`
|
AccessSecret string `json:"access_secret,omitempty"`
|
||||||
Bucket string `json:"bucket"`
|
Bucket string `json:"bucket,omitempty"`
|
||||||
UseSSL bool `json:"use_ssl"`
|
UseSSL bool `json:"use_ssl,omitempty"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QiNiuOssConfig struct {
|
type QiNiuOssConfig struct {
|
||||||
Zone string `json:"zone"`
|
Zone string `json:"zone,omitempty"`
|
||||||
AccessKey string `json:"access_key"`
|
AccessKey string `json:"access_key,omitempty"`
|
||||||
AccessSecret string `json:"access_secret"`
|
AccessSecret string `json:"access_secret,omitempty"`
|
||||||
Bucket string `json:"bucket"`
|
Bucket string `json:"bucket,omitempty"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AliYunOssConfig struct {
|
type AliYunOssConfig struct {
|
||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
AccessKey string `json:"access_key"`
|
AccessKey string `json:"access_key,omitempty"`
|
||||||
AccessSecret string `json:"access_secret"`
|
AccessSecret string `json:"access_secret,omitempty"`
|
||||||
Bucket string `json:"bucket"`
|
Bucket string `json:"bucket,omitempty"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalStorageConfig struct {
|
type LocalStorageConfig struct {
|
||||||
BasePath string `json:"base_path"`
|
BasePath string `json:"base_path,omitempty"`
|
||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
type PaymentConfig struct {
|
type PaymentConfig struct {
|
||||||
Alipay AlipayConfig `json:"alipay"` // 支付宝支付渠道配置
|
Alipay AlipayConfig `json:"alipay,omitempty"` // 支付宝支付渠道配置
|
||||||
Epay EpayConfig `json:"epay"` // 易支付配置
|
Epay EpayConfig `json:"epay,omitempty"` // 易支付配置
|
||||||
WxPay WxPayConfig `json:"wxpay"` // 微信支付渠道配置
|
WxPay WxPayConfig `json:"wxpay,omitempty"` // 微信支付渠道配置
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlipayConfig 支付宝支付配置
|
// AlipayConfig 支付宝支付配置
|
||||||
type AlipayConfig struct {
|
type AlipayConfig struct {
|
||||||
Enabled bool `json:"enabled"` // 是否启用该支付通道
|
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||||
SandBox bool `json:"sandbox"` // 是否沙盒环境
|
SandBox bool `json:"sandbox,omitempty"` // 是否沙盒环境
|
||||||
AppId string `json:"app_id"` // 应用 ID
|
AppId string `json:"app_id,omitempty"` // 应用 ID
|
||||||
PrivateKey string `json:"private_key"` // 应用私钥
|
PrivateKey string `json:"private_key,omitempty"` // 应用私钥
|
||||||
AlipayPublicKey string `json:"alipay_public_key"` // 支付宝公钥
|
AlipayPublicKey string `json:"alipay_public_key,omitempty"` // 支付宝公钥
|
||||||
Domain string `json:"domain"` // 支付回调域名
|
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AlipayConfig) Equal(other *AlipayConfig) bool {
|
func (c *AlipayConfig) Equal(other *AlipayConfig) bool {
|
||||||
@@ -25,13 +25,13 @@ func (c *AlipayConfig) Equal(other *AlipayConfig) bool {
|
|||||||
|
|
||||||
// WxPayConfig 微信支付配置
|
// WxPayConfig 微信支付配置
|
||||||
type WxPayConfig struct {
|
type WxPayConfig struct {
|
||||||
Enabled bool `json:"enabled"` // 是否启用该支付通道
|
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||||
AppId string `json:"app_id"` // 公众号的APPID,如:wxd678efh567hg6787
|
AppId string `json:"app_id,omitempty"` // 公众号的APPID,如:wxd678efh567hg6787
|
||||||
MchId string `json:"mch_id"` // 直连商户的商户号,由微信支付生成并下发
|
MchId string `json:"mch_id,omitempty"` // 直连商户的商户号,由微信支付生成并下发
|
||||||
SerialNo string `json:"serial_no"` // 商户证书的证书序列号
|
SerialNo string `json:"serial_no,omitempty"` // 商户证书的证书序列号
|
||||||
PrivateKey string `json:"private_key"` // 商户证书私钥
|
PrivateKey string `json:"private_key,omitempty"` // 商户证书私钥
|
||||||
ApiV3Key string `json:"api_v3_key"` // API V3 秘钥
|
ApiV3Key string `json:"api_v3_key,omitempty"` // API V3 秘钥
|
||||||
Domain string `json:"domain"` // 支付回调域名
|
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WxPayConfig) Equal(other *WxPayConfig) bool {
|
func (c *WxPayConfig) Equal(other *WxPayConfig) bool {
|
||||||
@@ -45,11 +45,11 @@ func (c *WxPayConfig) Equal(other *WxPayConfig) bool {
|
|||||||
|
|
||||||
// EpayConfig 易支付配置
|
// EpayConfig 易支付配置
|
||||||
type EpayConfig struct {
|
type EpayConfig struct {
|
||||||
Enabled bool `json:"enabled"` // 是否启用该支付通道
|
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
|
||||||
AppId string `json:"app_id"` // 商户 ID
|
AppId string `json:"app_id,omitempty"` // 商户 ID
|
||||||
PrivateKey string `json:"private_key"` // 私钥
|
PrivateKey string `json:"private_key,omitempty"` // 私钥
|
||||||
ApiURL string `json:"api_url"` // z支付 API 网关
|
ApiURL string `json:"api_url,omitempty"` // z支付 API 网关
|
||||||
Domain string `json:"domain"` // 支付回调域名
|
Domain string `json:"domain,omitempty"` // 支付回调域名
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *EpayConfig) Equal(other *EpayConfig) bool {
|
func (c *EpayConfig) Equal(other *EpayConfig) bool {
|
||||||
|
|||||||
@@ -8,23 +8,23 @@ package types
|
|||||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
type SMSConfig struct {
|
type SMSConfig struct {
|
||||||
Active string `json:"active"`
|
Active string `json:"active,omitempty"`
|
||||||
Ali SmsConfigAli `json:"aliyun"`
|
Ali SmsConfigAli `json:"aliyun,omitempty"`
|
||||||
Bao SmsConfigBao `json:"bao"`
|
Bao SmsConfigBao `json:"bao,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SmsConfigAli 阿里云短信平台配置
|
// SmsConfigAli 阿里云短信平台配置
|
||||||
type SmsConfigAli struct {
|
type SmsConfigAli struct {
|
||||||
AccessKey string `json:"access_key"`
|
AccessKey string `json:"access_key,omitempty"`
|
||||||
AccessSecret string `json:"access_secret"`
|
AccessSecret string `json:"access_secret,omitempty"`
|
||||||
Sign string `json:"sign"` // 短信签名
|
Sign string `json:"sign,omitempty"` // 短信签名
|
||||||
CodeTempId string `json:"code_temp_id"` // 验证码短信模板 ID
|
CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// SmsConfigBao 短信宝平台配置
|
// SmsConfigBao 短信宝平台配置
|
||||||
type SmsConfigBao struct {
|
type SmsConfigBao struct {
|
||||||
Username string `json:"username"` //短信宝平台注册的用户名
|
Username string `json:"username,omitempty"` //短信宝平台注册的用户名
|
||||||
Password string `json:"password"` //短信宝平台注册的密码
|
Password string `json:"password,omitempty"` //短信宝平台注册的密码
|
||||||
Sign string `json:"sign"` // 短信签名
|
Sign string `json:"sign,omitempty"` // 短信签名
|
||||||
CodeTemplate string `json:"code_template"` // 验证码短信模板 匹配
|
CodeTemplate string `json:"code_template,omitempty"` // 验证码短信模板 匹配
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ package types
|
|||||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
type SmtpConfig struct {
|
type SmtpConfig struct {
|
||||||
UseTls bool `json:"use_tls"` // 是否使用 TLS 发送
|
UseTls bool `json:"use_tls,omitempty"` // 是否使用 TLS 发送
|
||||||
Host string `json:"host"` // 邮件服务器地址
|
Host string `json:"host,omitempty"` // 邮件服务器地址
|
||||||
Port int `json:"port"` // 邮件服务器端口
|
Port int `json:"port,omitempty"` // 邮件服务器端口
|
||||||
AppName string `json:"app_name"` // 应用名称
|
AppName string `json:"app_name,omitempty"` // 应用名称
|
||||||
From string `json:"from"` // 发件人邮箱地址
|
From string `json:"from,omitempty"` // 发件人邮箱地址
|
||||||
Password string `json:"password"` // 发件人邮箱密码
|
Password string `json:"password,omitempty"` // 发件人邮箱密码
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SmtpConfig) Equal(other *SmtpConfig) bool {
|
func (s *SmtpConfig) Equal(other *SmtpConfig) bool {
|
||||||
|
|||||||
@@ -72,14 +72,13 @@ type SdTaskParams struct {
|
|||||||
type DallTask struct {
|
type DallTask struct {
|
||||||
ModelId uint `json:"model_id"`
|
ModelId uint `json:"model_id"`
|
||||||
ModelName string `json:"model_name"`
|
ModelName string `json:"model_name"`
|
||||||
|
ModelValue string `json:"model_value"`
|
||||||
Image []string `json:"image,omitempty"`
|
Image []string `json:"image,omitempty"`
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
UserId uint `json:"user_id"`
|
UserId uint `json:"user_id"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
N int `json:"n"`
|
AspectRatio string `json:"aspect_ratio"`
|
||||||
Quality string `json:"quality"`
|
|
||||||
Size string `json:"size"`
|
Size string `json:"size"`
|
||||||
Style string `json:"style"`
|
|
||||||
Power int `json:"power"`
|
Power int `json:"power"`
|
||||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
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()
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ require (
|
|||||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
github.com/shopspring/decimal v1.3.1
|
github.com/shopspring/decimal v1.3.1
|
||||||
github.com/syndtr/goleveldb v1.0.0
|
github.com/syndtr/goleveldb v1.0.0
|
||||||
|
github.com/volcengine/volcengine-go-sdk v1.1.34
|
||||||
golang.org/x/image v0.15.0
|
golang.org/x/image v0.15.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ require (
|
|||||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.uber.org/mock v0.4.0 // indirect
|
go.uber.org/mock v0.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
|||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
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 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
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.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
@@ -110,6 +111,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||||||
github.com/google/go-cmp v0.3.1/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.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.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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
|
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
|
||||||
@@ -259,6 +261,8 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
|
|||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
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 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
|
||||||
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
|
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
@@ -390,6 +394,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||||||
google.golang.org/protobuf v1.23.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.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.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 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import (
|
|||||||
|
|
||||||
var logger = logger2.GetLogger()
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
const SuperManagerID = 1
|
const SuperUsername = "admin"
|
||||||
|
|
||||||
type ManagerHandler struct {
|
type ManagerHandler struct {
|
||||||
handler.BaseHandler
|
handler.BaseHandler
|
||||||
@@ -94,7 +94,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 超级管理员默认是ID:1
|
// 超级管理员默认是ID:1
|
||||||
if manager.Id != SuperManagerID && manager.Status == false {
|
if manager.Username != SuperUsername && !manager.Status {
|
||||||
resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员")
|
resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
|||||||
IsSuperAdmin bool `json:"is_super_admin"`
|
IsSuperAdmin bool `json:"is_super_admin"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}{
|
}{
|
||||||
IsSuperAdmin: manager.Id == 1,
|
IsSuperAdmin: manager.Username == SuperUsername,
|
||||||
Token: tokenString,
|
Token: tokenString,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,12 +227,19 @@ func (h *ManagerHandler) Remove(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if id == SuperManagerID {
|
var user model.AdminUser
|
||||||
|
res := h.DB.Where("id", id).First(&user)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Username == SuperUsername {
|
||||||
resp.ERROR(c, "超级管理员不能删除")
|
resp.ERROR(c, "超级管理员不能删除")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := h.DB.Where("id", id).Delete(&model.AdminUser{})
|
res = h.DB.Where("id", id).Delete(&model.AdminUser{})
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, res.Error.Error())
|
resp.ERROR(c, res.Error.Error())
|
||||||
return
|
return
|
||||||
@@ -263,8 +270,14 @@ func (h *ManagerHandler) Enable(c *gin.Context) {
|
|||||||
|
|
||||||
// ResetPass 重置密码
|
// ResetPass 重置密码
|
||||||
func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
||||||
id := h.GetLoginUserId(c)
|
id := h.GetAdminId(c)
|
||||||
if id != SuperManagerID {
|
var user model.AdminUser
|
||||||
|
res := h.DB.Where("id", id).First(&user)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user.Username != SuperUsername {
|
||||||
resp.ERROR(c, "只有超级管理员能够进行该操作")
|
resp.ERROR(c, "只有超级管理员能够进行该操作")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -278,13 +291,6 @@ func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var user model.AdminUser
|
|
||||||
res := h.DB.Where("id", data.Id).First(&user)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, res.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
password := utils.GenPassword(data.Password, user.Salt)
|
password := utils.GenPassword(data.Password, user.Salt)
|
||||||
user.Password = password
|
user.Password = password
|
||||||
res = h.DB.Updates(&user)
|
res = h.DB.Updates(&user)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import (
|
|||||||
|
|
||||||
type ConfigHandler struct {
|
type ConfigHandler struct {
|
||||||
handler.BaseHandler
|
handler.BaseHandler
|
||||||
licenseService *service.LicenseService
|
|
||||||
sysConfig *types.SystemConfig
|
sysConfig *types.SystemConfig
|
||||||
alipayService *payment.AlipayService
|
alipayService *payment.AlipayService
|
||||||
wxpayService *payment.WxPayService
|
wxpayService *payment.WxPayService
|
||||||
@@ -41,7 +40,6 @@ type ConfigHandler struct {
|
|||||||
func NewConfigHandler(
|
func NewConfigHandler(
|
||||||
app *core.AppServer,
|
app *core.AppServer,
|
||||||
db *gorm.DB,
|
db *gorm.DB,
|
||||||
licenseService *service.LicenseService,
|
|
||||||
sysConfig *types.SystemConfig,
|
sysConfig *types.SystemConfig,
|
||||||
alipayService *payment.AlipayService,
|
alipayService *payment.AlipayService,
|
||||||
wxpayService *payment.WxPayService,
|
wxpayService *payment.WxPayService,
|
||||||
@@ -54,7 +52,6 @@ func NewConfigHandler(
|
|||||||
) *ConfigHandler {
|
) *ConfigHandler {
|
||||||
return &ConfigHandler{
|
return &ConfigHandler{
|
||||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||||
licenseService: licenseService,
|
|
||||||
sysConfig: sysConfig,
|
sysConfig: sysConfig,
|
||||||
alipayService: alipayService,
|
alipayService: alipayService,
|
||||||
wxpayService: wxpayService,
|
wxpayService: wxpayService,
|
||||||
@@ -87,8 +84,6 @@ func (h *ConfigHandler) RegisterRoutes() {
|
|||||||
rg.POST("update/oss", h.UpdateOss)
|
rg.POST("update/oss", h.UpdateOss)
|
||||||
rg.POST("update/smtp", h.UpdateStmp)
|
rg.POST("update/smtp", h.UpdateStmp)
|
||||||
rg.GET("get", h.Get)
|
rg.GET("get", h.Get)
|
||||||
rg.POST("license/active", h.Active)
|
|
||||||
rg.GET("license/get", h.GetLicense)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,19 +96,6 @@ func (h *ConfigHandler) UpdateBase(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未授权的话不允许修改版权
|
|
||||||
license := h.licenseService.GetLicense()
|
|
||||||
if !license.IsActive && data.Copyright != h.sysConfig.Base.Copyright {
|
|
||||||
resp.ERROR(c, "未授权系统不允许修改版权信息")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未授权的话不允许修改 Logo
|
|
||||||
if !license.IsActive && data.Logo != h.sysConfig.Base.Logo {
|
|
||||||
resp.ERROR(c, "未授权系统不允许修改 Logo")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := h.Update(types.ConfigKeySystem, data)
|
err := h.Update(types.ConfigKeySystem, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, err.Error())
|
resp.ERROR(c, err.Error())
|
||||||
@@ -368,10 +350,7 @@ func (h *ConfigHandler) UpdateWxLogin(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Enabled {
|
h.wxLoginService.UpdateConfig(data)
|
||||||
h.wxLoginService.UpdateConfig(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.sysConfig.WxLogin = data
|
h.sysConfig.WxLogin = data
|
||||||
resp.SUCCESS(c, data)
|
resp.SUCCESS(c, data)
|
||||||
}
|
}
|
||||||
@@ -410,36 +389,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
|||||||
|
|
||||||
resp.SUCCESS(c, value)
|
resp.SUCCESS(c, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active 激活系统
|
|
||||||
func (h *ConfigHandler) Active(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
License string `json:"license"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := h.licenseService.ActiveLicense(data.License)
|
|
||||||
license := h.licenseService.GetLicense()
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.Update(types.ConfigKeyLicense, license); err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 更新系统配置
|
|
||||||
h.sysConfig.License = *license
|
|
||||||
|
|
||||||
resp.SUCCESS(c, license.MachineId)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLicense 获取 License 信息
|
|
||||||
func (h *ConfigHandler) GetLicense(c *gin.Context) {
|
|
||||||
license := h.licenseService.GetLicense()
|
|
||||||
resp.SUCCESS(c, license)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
|
|||||||
continue // 跳过不存在的
|
continue // 跳过不存在的
|
||||||
}
|
}
|
||||||
tx := h.DB.Begin()
|
tx := h.DB.Begin()
|
||||||
if job.Status != model.JMTaskStatusSuccess && job.Power > 0 {
|
if job.Status != types.JMTaskStatusSuccess && job.Power > 0 {
|
||||||
remark := fmt.Sprintf("任务未成功,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
remark := fmt.Sprintf("任务未成功,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||||
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||||
Type: types.PowerRefund,
|
Type: types.PowerRefund,
|
||||||
@@ -172,7 +172,7 @@ func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
|
|||||||
// Stats 获取统计信息
|
// Stats 获取统计信息
|
||||||
func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
||||||
type StatResult struct {
|
type StatResult struct {
|
||||||
Status model.JMTaskStatus `json:"status"`
|
Status types.JMTaskStatus `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,13 +198,13 @@ func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
|||||||
for _, stat := range stats {
|
for _, stat := range stats {
|
||||||
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
|
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
|
||||||
switch stat.Status {
|
switch stat.Status {
|
||||||
case model.JMTaskStatusInQueue:
|
case types.JMTaskStatusInQueue:
|
||||||
result["pendingTasks"] = stat.Count
|
result["pendingTasks"] = stat.Count
|
||||||
case model.JMTaskStatusSuccess:
|
case types.JMTaskStatusSuccess:
|
||||||
result["completedTasks"] = stat.Count
|
result["completedTasks"] = stat.Count
|
||||||
case model.JMTaskStatusGenerating:
|
case types.JMTaskStatusGenerating:
|
||||||
result["processingTasks"] = stat.Count
|
result["processingTasks"] = stat.Count
|
||||||
case model.JMTaskStatusFailed:
|
case types.JMTaskStatusFailed:
|
||||||
result["failedTasks"] = stat.Count
|
result["failedTasks"] = stat.Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,29 +231,15 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证算力配置
|
// 验证算力配置
|
||||||
if req.Power.TextToImage <= 0 {
|
if len(req.Powers) == 0 {
|
||||||
resp.ERROR(c, "文生图算力必须大于0")
|
resp.ERROR(c, "请至少配置一个模型的积分")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Power.ImageToImage <= 0 {
|
for key, val := range req.Powers {
|
||||||
resp.ERROR(c, "图生图算力必须大于0")
|
if val <= 0 {
|
||||||
return
|
resp.ERROR(c, fmt.Sprintf("模型 %s 的积分必须大于0", key))
|
||||||
}
|
return
|
||||||
if req.Power.ImageEdit <= 0 {
|
}
|
||||||
resp.ERROR(c, "图片编辑算力必须大于0")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req.Power.ImageEffects <= 0 {
|
|
||||||
resp.ERROR(c, "图片特效算力必须大于0")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req.Power.TextToVideo <= 0 {
|
|
||||||
resp.ERROR(c, "文生视频算力必须大于0")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req.Power.ImageToVideo <= 0 {
|
|
||||||
resp.ERROR(c, "图生视频算力必须大于0")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"geekai/core/middleware"
|
"geekai/core/middleware"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/handler"
|
"geekai/handler"
|
||||||
"geekai/service"
|
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/store/vo"
|
"geekai/store/vo"
|
||||||
"geekai/utils"
|
"geekai/utils"
|
||||||
@@ -28,12 +27,11 @@ import (
|
|||||||
|
|
||||||
type UserHandler struct {
|
type UserHandler struct {
|
||||||
handler.BaseHandler
|
handler.BaseHandler
|
||||||
licenseService *service.LicenseService
|
redis *redis.Client
|
||||||
redis *redis.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService, redisCli *redis.Client) *UserHandler {
|
func NewUserHandler(app *core.AppServer, db *gorm.DB, redisCli *redis.Client) *UserHandler {
|
||||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService, redis: redisCli}
|
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, redis: redisCli}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRoutes 注册路由
|
// RegisterRoutes 注册路由
|
||||||
@@ -114,13 +112,6 @@ func (h *UserHandler) Save(c *gin.Context) {
|
|||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 检测最大注册人数
|
|
||||||
var totalUser int64
|
|
||||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
|
||||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
|
||||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var user = model.User{}
|
var user = model.User{}
|
||||||
var res *gorm.DB
|
var res *gorm.DB
|
||||||
var userVo vo.User
|
var userVo vo.User
|
||||||
@@ -198,11 +189,7 @@ func (h *UserHandler) Save(c *gin.Context) {
|
|||||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||||
}
|
}
|
||||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
|
||||||
} else {
|
|
||||||
u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
|
||||||
}
|
|
||||||
res = h.DB.Create(&u)
|
res = h.DB.Create(&u)
|
||||||
_ = utils.CopyObject(u, &userVo)
|
_ = utils.CopyObject(u, &userVo)
|
||||||
userVo.Id = u.Id
|
userVo.Id = u.Id
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"geekai/service"
|
"geekai/service"
|
||||||
"geekai/service/moderation"
|
"geekai/service/moderation"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
|
"geekai/service/sora"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/store/vo"
|
"geekai/store/vo"
|
||||||
"geekai/utils"
|
"geekai/utils"
|
||||||
@@ -65,34 +66,39 @@ type ChatHandler struct {
|
|||||||
BaseHandler
|
BaseHandler
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
uploadManager *oss.UploaderManager
|
uploadManager *oss.UploaderManager
|
||||||
licenseService *service.LicenseService
|
|
||||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
moderationManager *moderation.ServiceManager
|
moderationManager *moderation.ServiceManager
|
||||||
|
userLocks *types.UserLockManager
|
||||||
|
soraService *sora.SoraService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService, moderationManager *moderation.ServiceManager) *ChatHandler {
|
func NewChatHandler(app *core.AppServer,
|
||||||
|
db *gorm.DB,
|
||||||
|
redis *redis.Client,
|
||||||
|
manager *oss.UploaderManager,
|
||||||
|
userService *service.UserService,
|
||||||
|
soraService *sora.SoraService,
|
||||||
|
moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||||
return &ChatHandler{
|
return &ChatHandler{
|
||||||
BaseHandler: BaseHandler{App: app, DB: db},
|
BaseHandler: BaseHandler{App: app, DB: db},
|
||||||
redis: redis,
|
redis: redis,
|
||||||
uploadManager: manager,
|
uploadManager: manager,
|
||||||
licenseService: licenseService,
|
|
||||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||||
userService: userService,
|
userService: userService,
|
||||||
moderationManager: moderationManager,
|
moderationManager: moderationManager,
|
||||||
|
userLocks: types.NewUserLockManager(),
|
||||||
|
soraService: soraService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRoutes 注册路由
|
// RegisterRoutes 注册路由
|
||||||
func (h *ChatHandler) RegisterRoutes() {
|
func (h *ChatHandler) RegisterRoutes() {
|
||||||
group := h.App.Engine.Group("/api/chat/")
|
group := h.App.Engine.Group("/api/chat/")
|
||||||
|
|
||||||
// 聊天接口不需要授权(已在authConfig中配置)
|
|
||||||
group.Any("message", h.Chat)
|
|
||||||
|
|
||||||
// 其他接口需要用户授权
|
// 其他接口需要用户授权
|
||||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||||
{
|
{
|
||||||
|
group.Any("message", h.Chat)
|
||||||
group.GET("list", h.List)
|
group.GET("list", h.List)
|
||||||
group.GET("detail", h.Detail)
|
group.GET("detail", h.Detail)
|
||||||
group.POST("update", h.Update)
|
group.POST("update", h.Update)
|
||||||
@@ -120,6 +126,14 @@ func (h *ChatHandler) Chat(c *gin.Context) {
|
|||||||
return
|
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())
|
ctx, cancel := context.WithCancel(c.Request.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -262,9 +276,9 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
|||||||
if h.App.SysConfig.Base.ContextDeep > 0 {
|
if h.App.SysConfig.Base.ContextDeep > 0 {
|
||||||
var historyMessages []model.ChatMessage
|
var historyMessages []model.ChatMessage
|
||||||
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
|
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
|
||||||
if input.LastMsgId > 0 { // 重新生成逻辑
|
if input.LastMsgId > 0 { // 重新生成和编辑逻辑
|
||||||
var lastMessage model.ChatMessage
|
var lastMessage model.ChatMessage
|
||||||
err = dbSession.Where("id <= ?", input.LastMsgId).Where("type", types.PromptMsg).First(&lastMessage).Error
|
err = dbSession.Where("id < ?", input.LastMsgId).Where("type", types.ReplyMsg).Order("id DESC").First(&lastMessage).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
input.LastMsgId = 0
|
input.LastMsgId = 0
|
||||||
} else {
|
} else {
|
||||||
@@ -272,7 +286,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
|||||||
}
|
}
|
||||||
dbSession = dbSession.Where("id < ?", input.LastMsgId)
|
dbSession = dbSession.Where("id < ?", input.LastMsgId)
|
||||||
// 删除对应的聊天记录
|
// 删除对应的聊天记录
|
||||||
h.DB.Debug().Where("chat_id", input.ChatId).Where("id >= ?", input.LastMsgId).Delete(&model.ChatMessage{})
|
h.DB.Debug().Where("chat_id", input.ChatId).Where("id > ?", input.LastMsgId).Delete(&model.ChatMessage{})
|
||||||
}
|
}
|
||||||
err = dbSession.Limit(h.App.SysConfig.Base.ContextDeep).Order("id DESC").Find(&historyMessages).Error
|
err = dbSession.Limit(h.App.SysConfig.Base.ContextDeep).Order("id DESC").Find(&historyMessages).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -330,14 +344,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
|||||||
for _, file := range input.Files {
|
for _, file := range input.Files {
|
||||||
logger.Debugf("detected file: %+v", file.URL)
|
logger.Debugf("detected file: %+v", file.URL)
|
||||||
// 处理图片
|
// 处理图片
|
||||||
if isImageURL(file.URL) {
|
if isImageURL(file.URL) || isVideoURL(file.URL) {
|
||||||
imgList = append(imgList, gin.H{
|
imgList = append(imgList, gin.H{
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": gin.H{
|
"image_url": gin.H{
|
||||||
"url": file.URL,
|
"url": file.URL,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else if isTextURL(file.URL) {
|
||||||
// 处理文件,提取文件内容
|
// 处理文件,提取文件内容
|
||||||
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -398,23 +412,43 @@ func isImageURL(url string) bool {
|
|||||||
".svg": true,
|
".svg": true,
|
||||||
".ico": true,
|
".ico": true,
|
||||||
}
|
}
|
||||||
|
return validImageExts[ext]
|
||||||
|
}
|
||||||
|
|
||||||
if !validImageExts[ext] {
|
// 判断是个链接是否是文本链接
|
||||||
|
func isTextURL(url string) bool {
|
||||||
|
// 检查是否是有效的URL
|
||||||
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送HEAD请求检查Content-Type
|
// 检查文件扩展名
|
||||||
client := &http.Client{
|
ext := strings.ToLower(path.Ext(url))
|
||||||
Timeout: 5 * time.Second,
|
validTextExts := map[string]bool{
|
||||||
|
".txt": true,
|
||||||
|
".doc": true,
|
||||||
|
".docx": true,
|
||||||
|
".pdf": true,
|
||||||
}
|
}
|
||||||
resp, err := client.Head(url)
|
return validTextExts[ext]
|
||||||
if err != nil {
|
}
|
||||||
|
|
||||||
|
// 判断是个链接是否是视频
|
||||||
|
func isVideoURL(url string) bool {
|
||||||
|
// 检查是否是有效的URL
|
||||||
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
// 检查文件扩展名
|
||||||
return strings.HasPrefix(contentType, "image/")
|
ext := strings.ToLower(path.Ext(url))
|
||||||
|
validVideoExts := map[string]bool{
|
||||||
|
".mp4": true,
|
||||||
|
".avi": true,
|
||||||
|
".mov": true,
|
||||||
|
".wmv": true,
|
||||||
|
}
|
||||||
|
return validVideoExts[ext]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tokens 统计 token 数量
|
// Tokens 统计 token 数量
|
||||||
@@ -495,11 +529,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input
|
|||||||
return nil, errors.New("no available key, please import key")
|
return nil, errors.New("no available key, please import key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ONLY allow apiURL in blank list
|
|
||||||
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Debugf("对话请求消息体:%+v", req)
|
logger.Debugf("对话请求消息体:%+v", req)
|
||||||
var apiURL string
|
var apiURL string
|
||||||
p, _ := url.Parse(apiKey.ApiURL)
|
p, _ := url.Parse(apiKey.ApiURL)
|
||||||
@@ -634,6 +663,16 @@ func (h *ChatHandler) saveChatHistory(
|
|||||||
replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
||||||
totalTokens = replyTokens + getTotalTokens(req)
|
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 {
|
||||||
|
files = append(files, *video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
historyReplyMsg := model.ChatMessage{
|
historyReplyMsg := model.ChatMessage{
|
||||||
UserId: userVo.Id,
|
UserId: userVo.Id,
|
||||||
ChatId: input.ChatId,
|
ChatId: input.ChatId,
|
||||||
@@ -642,7 +681,7 @@ func (h *ChatHandler) saveChatHistory(
|
|||||||
Icon: input.ChatRole.Icon,
|
Icon: input.ChatRole.Icon,
|
||||||
Content: utils.JsonEncode(vo.MsgContent{
|
Content: utils.JsonEncode(vo.MsgContent{
|
||||||
Text: message.Content,
|
Text: message.Content,
|
||||||
Files: input.Files,
|
Files: files,
|
||||||
}),
|
}),
|
||||||
Tokens: replyTokens,
|
Tokens: replyTokens,
|
||||||
TotalTokens: totalTokens,
|
TotalTokens: totalTokens,
|
||||||
@@ -666,6 +705,7 @@ func (h *ChatHandler) saveChatHistory(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
content.Text = historyReplyMsg.Content
|
content.Text = historyReplyMsg.Content
|
||||||
}
|
}
|
||||||
|
content.Files = files
|
||||||
messageVo.Content = content
|
messageVo.Content = content
|
||||||
messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix()
|
messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix()
|
||||||
messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix()
|
messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix()
|
||||||
|
|||||||
@@ -212,6 +212,14 @@ func (h *ChatHandler) sendOpenAiMessage(
|
|||||||
} else {
|
} else {
|
||||||
errMsg = utils.InterfaceToString(apiRes.Data)
|
errMsg = utils.InterfaceToString(apiRes.Data)
|
||||||
contents = append(contents, errMsg)
|
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)
|
pushMessage(c, "text", errMsg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"geekai/core"
|
"geekai/core"
|
||||||
"geekai/service"
|
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/utils"
|
"geekai/utils"
|
||||||
"geekai/utils/resp"
|
"geekai/utils/resp"
|
||||||
@@ -20,11 +19,10 @@ import (
|
|||||||
|
|
||||||
type ConfigHandler struct {
|
type ConfigHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
licenseService *service.LicenseService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler {
|
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
|
||||||
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService}
|
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRoutes 注册路由
|
// RegisterRoutes 注册路由
|
||||||
@@ -33,7 +31,6 @@ func (h *ConfigHandler) RegisterRoutes() {
|
|||||||
|
|
||||||
// 无需授权的接口
|
// 无需授权的接口
|
||||||
group.GET("get", h.Get)
|
group.GET("get", h.Get)
|
||||||
group.GET("license", h.License)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 获取指定的系统配置
|
// Get 获取指定的系统配置
|
||||||
@@ -55,9 +52,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
|||||||
|
|
||||||
resp.SUCCESS(c, value)
|
resp.SUCCESS(c, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// License 获取 License 配置
|
|
||||||
func (h *ConfigHandler) License(c *gin.Context) {
|
|
||||||
license := h.licenseService.GetLicense()
|
|
||||||
resp.SUCCESS(c, license.Configs)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -117,12 +117,12 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
|||||||
task := types.DallTask{
|
task := types.DallTask{
|
||||||
UserId: uint(userId),
|
UserId: uint(userId),
|
||||||
ModelId: chatModel.Id,
|
ModelId: chatModel.Id,
|
||||||
ModelName: chatModel.Value,
|
ModelName: chatModel.Name,
|
||||||
|
ModelValue: chatModel.Value,
|
||||||
Image: data.Image,
|
Image: data.Image,
|
||||||
Prompt: data.Prompt,
|
Prompt: data.Prompt,
|
||||||
Quality: data.Quality,
|
AspectRatio: data.AspectRatio,
|
||||||
Size: data.Size,
|
Size: data.Size,
|
||||||
Style: data.Style,
|
|
||||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||||
Power: chatModel.Power,
|
Power: chatModel.Power,
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
|||||||
// 扣减算力
|
// 扣减算力
|
||||||
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
|
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
|
||||||
Type: types.PowerConsume,
|
Type: types.PowerConsume,
|
||||||
Model: chatModel.Value,
|
Model: chatModel.Name,
|
||||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -215,12 +215,9 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
|||||||
UserId: user.Id,
|
UserId: user.Id,
|
||||||
Prompt: prompt,
|
Prompt: prompt,
|
||||||
ModelId: chatModel.Id,
|
ModelId: chatModel.Id,
|
||||||
ModelName: chatModel.Value,
|
ModelValue: chatModel.Value,
|
||||||
|
ModelName: chatModel.Name,
|
||||||
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
|
||||||
N: 1,
|
|
||||||
Quality: "standard",
|
|
||||||
Size: "1024x1024",
|
|
||||||
Style: "vivid",
|
|
||||||
Power: chatModel.Power,
|
Power: chatModel.Power,
|
||||||
}
|
}
|
||||||
job := model.DallJob{
|
job := model.DallJob{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"geekai/core"
|
"geekai/core"
|
||||||
"geekai/core/middleware"
|
"geekai/core/middleware"
|
||||||
@@ -38,49 +39,28 @@ func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *go
|
|||||||
// RegisterRoutes 注册路由,新增统一任务接口
|
// RegisterRoutes 注册路由,新增统一任务接口
|
||||||
func (h *JimengHandler) RegisterRoutes() {
|
func (h *JimengHandler) RegisterRoutes() {
|
||||||
group := h.App.Engine.Group("/api/jimeng/")
|
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.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||||
{
|
{
|
||||||
group.POST("task", h.CreateTask)
|
group.POST("task", h.CreateTask)
|
||||||
group.GET("power-config", h.GetPowerConfig)
|
|
||||||
group.POST("jobs", h.Jobs)
|
group.POST("jobs", h.Jobs)
|
||||||
group.GET("remove", h.Remove)
|
group.GET("remove", h.Remove)
|
||||||
group.GET("retry", h.Retry)
|
group.GET("retry", h.Retry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JimengTaskRequest 统一任务请求结构体
|
|
||||||
// 支持所有生图和生成视频类型
|
|
||||||
type JimengTaskRequest struct {
|
|
||||||
TaskType string `json:"task_type" binding:"required"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
ImageInput string `json:"image_input"`
|
|
||||||
ImageUrls []string `json:"image_urls"`
|
|
||||||
BinaryDataBase64 []string `json:"binary_data_base64"`
|
|
||||||
Scale float64 `json:"scale"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
Gpen float64 `json:"gpen"`
|
|
||||||
Skin float64 `json:"skin"`
|
|
||||||
SkinUnifi float64 `json:"skin_unifi"`
|
|
||||||
GenMode string `json:"gen_mode"`
|
|
||||||
Seed int64 `json:"seed"`
|
|
||||||
UsePreLLM bool `json:"use_pre_llm"`
|
|
||||||
TemplateId string `json:"template_id"`
|
|
||||||
AspectRatio string `json:"aspect_ratio"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTask 统一任务创建接口
|
// CreateTask 统一任务创建接口
|
||||||
func (h *JimengHandler) CreateTask(c *gin.Context) {
|
func (h *JimengHandler) CreateTask(c *gin.Context) {
|
||||||
var req JimengTaskRequest
|
var req types.JimengTaskRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文本审核
|
// 文本审核
|
||||||
if h.App.SysConfig.Moderation.Enable {
|
if h.App.SysConfig.Moderation.Enable && req.Prompt != "" {
|
||||||
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
|
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to moderate content: ", err)
|
logger.Error("failed to moderate content: ", err)
|
||||||
@@ -103,136 +83,21 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:除图像特效外,其他任务类型必须有提示词
|
if req.Prompt == "" && len(req.ImageUrls) == 0 {
|
||||||
if req.TaskType != "image_effects" && req.Prompt == "" {
|
resp.ERROR(c, "提示词和图片不能同时为空")
|
||||||
resp.ERROR(c, "提示词不能为空")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.GetLoginUser(c)
|
user, err := h.GetLoginUser(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.NotAuth(c)
|
resp.NotAuth(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Width == 0 {
|
// 获取算力消耗
|
||||||
req.Width = 1328
|
powerCost, err := h.getTaskPower(req)
|
||||||
}
|
if err != nil {
|
||||||
if req.Height == 0 {
|
resp.ERROR(c, "计算任务消耗积分失败: "+err.Error())
|
||||||
req.Height = 1328
|
|
||||||
}
|
|
||||||
if req.Seed == 0 {
|
|
||||||
req.Seed = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
var powerCost int
|
|
||||||
var taskType model.JMTaskType
|
|
||||||
var params map[string]any
|
|
||||||
var reqKey string
|
|
||||||
var modelName string
|
|
||||||
|
|
||||||
switch req.TaskType {
|
|
||||||
case "text_to_image":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToImage)
|
|
||||||
taskType = model.JMTaskTypeTextToImage
|
|
||||||
reqKey = jimeng.ReqKeyTextToImage
|
|
||||||
modelName = "即梦文生图"
|
|
||||||
if req.Scale == 0 {
|
|
||||||
req.Scale = 2.5
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"seed": req.Seed,
|
|
||||||
"scale": req.Scale,
|
|
||||||
"width": req.Width,
|
|
||||||
"height": req.Height,
|
|
||||||
"use_pre_llm": req.UsePreLLM,
|
|
||||||
}
|
|
||||||
case "image_to_image":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToImage)
|
|
||||||
taskType = model.JMTaskTypeImageToImage
|
|
||||||
reqKey = jimeng.ReqKeyImageToImagePortrait
|
|
||||||
modelName = "即梦图生图"
|
|
||||||
if req.Gpen == 0 {
|
|
||||||
req.Gpen = 0.4
|
|
||||||
}
|
|
||||||
if req.Skin == 0 {
|
|
||||||
req.Skin = 0.3
|
|
||||||
}
|
|
||||||
if req.GenMode == "" {
|
|
||||||
if req.Prompt != "" {
|
|
||||||
req.GenMode = jimeng.GenModeCreative
|
|
||||||
} else {
|
|
||||||
req.GenMode = jimeng.GenModeReference
|
|
||||||
}
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"image_input": req.ImageInput,
|
|
||||||
"width": req.Width,
|
|
||||||
"height": req.Height,
|
|
||||||
"gpen": req.Gpen,
|
|
||||||
"skin": req.Skin,
|
|
||||||
"skin_unifi": req.SkinUnifi,
|
|
||||||
"gen_mode": req.GenMode,
|
|
||||||
"seed": req.Seed,
|
|
||||||
}
|
|
||||||
case "image_edit":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEdit)
|
|
||||||
taskType = model.JMTaskTypeImageEdit
|
|
||||||
reqKey = jimeng.ReqKeyImageEdit
|
|
||||||
modelName = "即梦图像编辑"
|
|
||||||
if req.Scale == 0 {
|
|
||||||
req.Scale = 0.5
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"seed": req.Seed,
|
|
||||||
"scale": req.Scale,
|
|
||||||
}
|
|
||||||
params["image_urls"] = []string{req.ImageInput}
|
|
||||||
case "image_effects":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEffects)
|
|
||||||
taskType = model.JMTaskTypeImageEffects
|
|
||||||
reqKey = jimeng.ReqKeyImageEffects
|
|
||||||
modelName = "即梦图像特效"
|
|
||||||
if req.Width == 0 {
|
|
||||||
req.Width = 1328
|
|
||||||
}
|
|
||||||
if req.Height == 0 {
|
|
||||||
req.Height = 1328
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"image_input1": req.ImageInput,
|
|
||||||
"template_id": req.TemplateId,
|
|
||||||
"width": req.Width,
|
|
||||||
"height": req.Height,
|
|
||||||
}
|
|
||||||
case "text_to_video":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToVideo)
|
|
||||||
taskType = model.JMTaskTypeTextToVideo
|
|
||||||
reqKey = jimeng.ReqKeyTextToVideo
|
|
||||||
modelName = "即梦文生视频"
|
|
||||||
if req.AspectRatio == "" {
|
|
||||||
req.AspectRatio = jimeng.AspectRatio16_9
|
|
||||||
}
|
|
||||||
params = map[string]any{
|
|
||||||
"seed": req.Seed,
|
|
||||||
"aspect_ratio": req.AspectRatio,
|
|
||||||
}
|
|
||||||
case "image_to_video":
|
|
||||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToVideo)
|
|
||||||
taskType = model.JMTaskTypeImageToVideo
|
|
||||||
reqKey = jimeng.ReqKeyImageToVideo
|
|
||||||
modelName = "即梦图生视频"
|
|
||||||
params = map[string]any{
|
|
||||||
"seed": req.Seed,
|
|
||||||
"aspect_ratio": req.AspectRatio,
|
|
||||||
}
|
|
||||||
if len(req.ImageUrls) > 0 {
|
|
||||||
params["image_urls"] = req.ImageUrls
|
|
||||||
}
|
|
||||||
if len(req.BinaryDataBase64) > 0 {
|
|
||||||
params["binary_data_base64"] = req.BinaryDataBase64
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
resp.ERROR(c, "不支持的任务类型")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,16 +105,9 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
|
|||||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.Power = powerCost
|
||||||
|
|
||||||
taskReq := &jimeng.CreateTaskRequest{
|
job, err := h.jimengService.CreateTask(user.Id, &req)
|
||||||
Type: taskType,
|
|
||||||
Prompt: req.Prompt,
|
|
||||||
Params: params,
|
|
||||||
ReqKey: reqKey,
|
|
||||||
Power: powerCost,
|
|
||||||
}
|
|
||||||
|
|
||||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("create jimeng task failed: %v", err)
|
logger.Errorf("create jimeng task failed: %v", err)
|
||||||
resp.ERROR(c, "创建任务失败")
|
resp.ERROR(c, "创建任务失败")
|
||||||
@@ -258,11 +116,42 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
|
|||||||
|
|
||||||
h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
|
h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
|
||||||
Type: types.PowerConsume,
|
Type: types.PowerConsume,
|
||||||
Model: "jimeng",
|
Model: job.ReqKey,
|
||||||
Remark: fmt.Sprintf("%s,任务ID:%d", modelName, job.Id),
|
Remark: h.getTaskRemark(req, job.Id),
|
||||||
})
|
})
|
||||||
|
|
||||||
resp.SUCCESS(c, job)
|
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 获取任务列表
|
// Jobs 获取任务列表
|
||||||
@@ -287,17 +176,13 @@ func (h *JimengHandler) Jobs(c *gin.Context) {
|
|||||||
|
|
||||||
switch req.Filter {
|
switch req.Filter {
|
||||||
case "image":
|
case "image":
|
||||||
query = query.Where("type IN (?)", []model.JMTaskType{
|
query = query.Where("type = ?", types.JMTaskTypeImage)
|
||||||
model.JMTaskTypeTextToImage,
|
|
||||||
model.JMTaskTypeImageToImage,
|
|
||||||
model.JMTaskTypeImageEdit,
|
|
||||||
model.JMTaskTypeImageEffects,
|
|
||||||
})
|
|
||||||
case "video":
|
case "video":
|
||||||
query = query.Where("type IN (?)", []model.JMTaskType{
|
query = query.Where("type = ?", types.JMTaskTypeVideo)
|
||||||
model.JMTaskTypeTextToVideo,
|
case "virtual_human":
|
||||||
model.JMTaskTypeImageToVideo,
|
query = query.Where("type = ?", types.JMTaskTypeVirtualHuman)
|
||||||
})
|
case "action_transfer":
|
||||||
|
query = query.Where("type = ?", types.JMTaskTypeActionTransfer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Ids) > 0 {
|
if len(req.Ids) > 0 {
|
||||||
@@ -357,34 +242,17 @@ func (h *JimengHandler) Remove(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 正在运行中的任务不能删除
|
// 正在运行中的任务不能删除
|
||||||
if job.Status == model.JMTaskStatusGenerating || job.Status == model.JMTaskStatusInQueue {
|
if job.Status == types.JMTaskStatusGenerating || job.Status == types.JMTaskStatusInQueue {
|
||||||
resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力")
|
resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := h.DB.Begin()
|
if err := h.DB.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||||
if err := tx.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
|
||||||
logger.Errorf("delete jimeng job failed: %v", err)
|
logger.Errorf("delete jimeng job failed: %v", err)
|
||||||
resp.ERROR(c, "删除任务失败")
|
resp.ERROR(c, "删除任务失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 失败任务删除后退回算力
|
|
||||||
if job.Status != model.JMTaskStatusFailed {
|
|
||||||
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
|
||||||
Type: types.PowerRefund,
|
|
||||||
Model: "jimeng",
|
|
||||||
Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "退回算力失败")
|
|
||||||
tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.Commit()
|
|
||||||
|
|
||||||
resp.SUCCESS(c, gin.H{})
|
resp.SUCCESS(c, gin.H{})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,13 +279,13 @@ func (h *JimengHandler) Retry(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 只有失败的任务才能重试
|
// 只有失败的任务才能重试
|
||||||
if job.Status != model.JMTaskStatusFailed {
|
if job.Status != types.JMTaskStatusFailed {
|
||||||
resp.ERROR(c, "只有失败的任务才能重试")
|
resp.ERROR(c, "只有失败的任务才能重试")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置任务状态
|
// 重置任务状态
|
||||||
if err := h.jimengService.UpdateJobStatus(uint(jobId), model.JMTaskStatusInQueue, ""); err != nil {
|
if err := h.jimengService.UpdateJobStatus(uint(jobId), types.JMTaskStatusInQueue, ""); err != nil {
|
||||||
logger.Errorf("reset job status failed: %v", err)
|
logger.Errorf("reset job status failed: %v", err)
|
||||||
resp.ERROR(c, "重置任务状态失败")
|
resp.ERROR(c, "重置任务状态失败")
|
||||||
return
|
return
|
||||||
@@ -433,25 +301,49 @@ func (h *JimengHandler) Retry(c *gin.Context) {
|
|||||||
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
|
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPowerFromConfig 从配置中获取指定类型的算力消耗
|
func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
|
||||||
func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
|
logger.Debugf("getTaskPower req: %+v", req)
|
||||||
config := h.App.SysConfig.Jimeng
|
config := h.App.SysConfig.Jimeng
|
||||||
|
basePower, ok := config.Powers[req.ReqKey]
|
||||||
switch taskType {
|
if !ok || basePower <= 0 {
|
||||||
case model.JMTaskTypeTextToImage:
|
return 0, errors.New("未配置模型积分或配置不合法")
|
||||||
return config.Power.TextToImage
|
}
|
||||||
case model.JMTaskTypeImageToImage:
|
switch req.TaskType {
|
||||||
return config.Power.ImageToImage
|
case types.JMTaskTypeImage:
|
||||||
case model.JMTaskTypeImageEdit:
|
return basePower, nil
|
||||||
return config.Power.ImageEdit
|
case types.JMTaskTypeVideo:
|
||||||
case model.JMTaskTypeImageEffects:
|
if req.Duration == 0 {
|
||||||
return config.Power.ImageEffects
|
return 0, errors.New("视频时长不能为0")
|
||||||
case model.JMTaskTypeTextToVideo:
|
}
|
||||||
return config.Power.TextToVideo
|
return basePower * req.Duration, nil
|
||||||
case model.JMTaskTypeImageToVideo:
|
case types.JMTaskTypeVirtualHuman:
|
||||||
return config.Power.ImageToVideo
|
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:
|
default:
|
||||||
return 10
|
return 0, errors.New("任务类型不支持")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,11 +351,6 @@ func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
|
|||||||
func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
|
func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
|
||||||
config := h.App.SysConfig.Jimeng
|
config := h.App.SysConfig.Jimeng
|
||||||
resp.SUCCESS(c, gin.H{
|
resp.SUCCESS(c, gin.H{
|
||||||
"text_to_image": config.Power.TextToImage,
|
"powers": config.Powers,
|
||||||
"image_to_image": config.Power.ImageToImage,
|
|
||||||
"image_edit": config.Power.ImageEdit,
|
|
||||||
"image_effects": config.Power.ImageEffects,
|
|
||||||
"text_to_video": config.Power.TextToVideo,
|
|
||||||
"image_to_video": config.Power.ImageToVideo,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
|||||||
func (h *MenuHandler) RegisterRoutes() {
|
func (h *MenuHandler) RegisterRoutes() {
|
||||||
group := h.App.Engine.Group("/api/menu/")
|
group := h.App.Engine.Group("/api/menu/")
|
||||||
group.GET("list", h.List)
|
group.GET("list", h.List)
|
||||||
|
group.GET("list/all", h.ListAll)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List 数据列表
|
// List 数据列表
|
||||||
@@ -54,3 +55,20 @@ func (h *MenuHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
resp.SUCCESS(c, list)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ func (h *PaymentHandler) SyncOrders() error {
|
|||||||
}
|
}
|
||||||
case payment.PayChannelAL:
|
case payment.PayChannelAL:
|
||||||
res, err = h.alipayService.Query(order.OrderNo)
|
res, err = h.alipayService.Query(order.OrderNo)
|
||||||
|
logger.Debugf("支付宝支付订单状态:%+v", res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("error with query order info: %v", err)
|
logger.Errorf("error with query order info: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ type UserHandler struct {
|
|||||||
searcher *xdb.Searcher
|
searcher *xdb.Searcher
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
levelDB *store.LevelDB
|
levelDB *store.LevelDB
|
||||||
licenseService *service.LicenseService
|
|
||||||
captchaService *service.CaptchaService
|
captchaService *service.CaptchaService
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
wxLoginService *service.WxLoginService
|
wxLoginService *service.WxLoginService
|
||||||
@@ -51,15 +50,13 @@ func NewUserHandler(
|
|||||||
captcha *service.CaptchaService,
|
captcha *service.CaptchaService,
|
||||||
userService *service.UserService,
|
userService *service.UserService,
|
||||||
wxLoginService *service.WxLoginService,
|
wxLoginService *service.WxLoginService,
|
||||||
ipSearcher *xdb.Searcher,
|
ipSearcher *xdb.Searcher) *UserHandler {
|
||||||
licenseService *service.LicenseService) *UserHandler {
|
|
||||||
return &UserHandler{
|
return &UserHandler{
|
||||||
BaseHandler: BaseHandler{DB: db, App: app},
|
BaseHandler: BaseHandler{DB: db, App: app},
|
||||||
searcher: searcher,
|
searcher: searcher,
|
||||||
redis: client,
|
redis: client,
|
||||||
levelDB: levelDB,
|
levelDB: levelDB,
|
||||||
captchaService: captcha,
|
captchaService: captcha,
|
||||||
licenseService: licenseService,
|
|
||||||
userService: userService,
|
userService: userService,
|
||||||
wxLoginService: wxLoginService,
|
wxLoginService: wxLoginService,
|
||||||
ipSearcher: ipSearcher,
|
ipSearcher: ipSearcher,
|
||||||
@@ -132,14 +129,6 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测最大注册人数
|
|
||||||
var totalUser int64
|
|
||||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
|
||||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
|
||||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查验证码
|
// 检查验证码
|
||||||
var key string
|
var key string
|
||||||
if data.RegWay == "email" {
|
if data.RegWay == "email" {
|
||||||
@@ -327,7 +316,7 @@ func (h *UserHandler) GetWxLoginState(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createNewUser 创建新用户
|
// createNewUser 创建新用户
|
||||||
func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.User, error) {
|
func (h *UserHandler) createNewUser(user model.User, code string) (model.User, error) {
|
||||||
if user.OpenId != "" {
|
if user.OpenId != "" {
|
||||||
user.Platform = "wechat"
|
user.Platform = "wechat"
|
||||||
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
|
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
|
||||||
@@ -353,19 +342,22 @@ func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.U
|
|||||||
// 创建用户
|
// 创建用户
|
||||||
tx := h.DB.Begin()
|
tx := h.DB.Begin()
|
||||||
if err := tx.Create(&user).Error; err != nil {
|
if err := tx.Create(&user).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录邀请关系
|
// 记录邀请关系
|
||||||
if inviteCode != "" {
|
if code != "" {
|
||||||
inviteCode := model.InviteCode{}
|
var inviteCode model.InviteCode
|
||||||
err := h.DB.Where("code = ?", inviteCode).First(&inviteCode).Error
|
err := h.DB.Where("code = ?", code).First(&inviteCode).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Error("无效的邀请码:", err.Error())
|
||||||
|
tx.Rollback()
|
||||||
return user, fmt.Errorf("无效的邀请码")
|
return user, fmt.Errorf("无效的邀请码")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加邀请数量
|
// 增加邀请数量
|
||||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", inviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||||
if h.App.SysConfig.Base.InvitePower > 0 {
|
if h.App.SysConfig.Base.InvitePower > 0 {
|
||||||
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
|
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
|
||||||
Type: types.PowerInvite,
|
Type: types.PowerInvite,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"geekai/service/payment"
|
"geekai/service/payment"
|
||||||
"geekai/service/sd"
|
"geekai/service/sd"
|
||||||
"geekai/service/sms"
|
"geekai/service/sms"
|
||||||
|
"geekai/service/sora"
|
||||||
"geekai/service/suno"
|
"geekai/service/suno"
|
||||||
"geekai/service/video"
|
"geekai/service/video"
|
||||||
"geekai/store"
|
"geekai/store"
|
||||||
@@ -166,12 +167,6 @@ func main() {
|
|||||||
|
|
||||||
// 邮件服务
|
// 邮件服务
|
||||||
fx.Provide(service.NewSmtpService),
|
fx.Provide(service.NewSmtpService),
|
||||||
// License 服务
|
|
||||||
fx.Provide(service.NewLicenseService),
|
|
||||||
fx.Invoke(func(licenseService *service.LicenseService) {
|
|
||||||
licenseService.SyncLicense()
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Dalle 服务
|
// Dalle 服务
|
||||||
fx.Provide(dalle.NewService),
|
fx.Provide(dalle.NewService),
|
||||||
fx.Invoke(func(s *dalle.Service) {
|
fx.Invoke(func(s *dalle.Service) {
|
||||||
@@ -189,6 +184,9 @@ func main() {
|
|||||||
s.DownloadImages()
|
s.DownloadImages()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Sora service
|
||||||
|
fx.Provide(sora.NewSoraService),
|
||||||
|
|
||||||
// Stable Diffusion 机器人
|
// Stable Diffusion 机器人
|
||||||
fx.Provide(sd.NewService),
|
fx.Provide(sd.NewService),
|
||||||
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
|
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
|
||||||
@@ -222,12 +220,8 @@ func main() {
|
|||||||
fx.Provide(sms.NewAliYunSmsService),
|
fx.Provide(sms.NewAliYunSmsService),
|
||||||
fx.Provide(sms.NewBaoSmsService),
|
fx.Provide(sms.NewBaoSmsService),
|
||||||
fx.Provide(sms.NewSmsManager),
|
fx.Provide(sms.NewSmsManager),
|
||||||
fx.Provide(func(config *types.SystemConfig) *service.CaptchaService {
|
fx.Provide(service.NewCaptchaService),
|
||||||
return service.NewCaptchaService(config.Captcha)
|
fx.Provide(service.NewWxLoginService),
|
||||||
}),
|
|
||||||
fx.Provide(func(config *types.SystemConfig, client *redis.Client) *service.WxLoginService {
|
|
||||||
return service.NewWxLoginService(config.WxLogin, client)
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 支付服务
|
// 支付服务
|
||||||
fx.Provide(payment.NewAlipayService),
|
fx.Provide(payment.NewAlipayService),
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDszCCApugAwIBAgIQICMRB0rBU2/rZJbfJGMYIzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
|
|
||||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
|
|
||||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
|
|
||||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDYzNTQxWhcNMjQxMTA2MDYzNTQxWjCB
|
|
||||||
hDELMAkGA1UEBhMCQ04xHzAdBgNVBAoMFm1ib25meTkwMTVAc2FuZGJveC5jb20xDzANBgNVBAsM
|
|
||||||
BkFsaXBheTFDMEEGA1UEAww65pSv5LuY5a6dKOS4reWbvSnnvZHnu5zmioDmnK/mnInpmZDlhazl
|
|
||||||
j7gtMjA4ODcyMTAyMDc1MDU4MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsoKcw5
|
|
||||||
sxaiyV7mpWzDtnQ1K518eQLP0+dJlZAf06aBep/Aj9DIqrba/k7DHt8dKQvILMLAMpN1+2IRxbaO
|
|
||||||
yxMa/laj3lZ1eHrB6F077O3D62oHcE3noZtXL0N1zZAxpmkNmYIHeLZS2oLMS4ANu47O/wpDC7BV
|
|
||||||
HjdpZugtdPJ4mxdCpM9GDdLs7W4s5QI4PUPK4skFNMFoKI+0cYP/9ju87UP//IHC/K510GWNl+Gn
|
|
||||||
Cvgag3AmiIB0utJNsGhxm6zT1T9tUWjW9iz/BxBKiPatsCX9VpPQzGnW7ZonRQtiZSokIlP2IPvl
|
|
||||||
H5DcwpWUz3/LUY0SmKxnKOEYeOOqCW8CAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
|
|
||||||
DQEBCwUAA4IBAQAtgxF2EzjOndEFxBUD9tFwcSt6XKGggOp52oft1pvynPg4ALTLafOtfEPDrFBH
|
|
||||||
PwpYrSu9s9C8NJtaA2HrlCfBjIuwEFTXiN+HPvS0SwSPKt9AXEiTcOF8vDcGamEen8QI4fo5Jia7
|
|
||||||
2VRKkerkww5/+FzSaVO7ZUKuL80M1QJStmAZc8kPPwdYOTTW2bGf8BcmSDL6SPElBkt7tCCRd4sn
|
|
||||||
+jq4cZ0yb2i77rBZCwHcTvfTqIBblPwLv4uGvg3+83BxIB5w6Kqp06bKEAPmobFY5IVHa+ON0/qi
|
|
||||||
BXxXr+WQ3piKRVQEN64+PTAjSc67Ix1umvpLl3Ko6Ry7NJmpDcUn
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDszCCApugAwIBAgIQIBkIGbgVxq210KxLJ+YA/TANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UE
|
|
||||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxJTAjBgNVBAsMHENlcnRpZmljYXRpb24gQXV0
|
|
||||||
aG9yaXR5IHRlc3QxNjA0BgNVBAMMLUFudCBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
|
|
||||||
dHkgUjEgdGVzdDAeFw0xOTA4MTkxMTE2MDBaFw0yNDA4MDExMTE2MDBaMIGRMQswCQYDVQQGEwJD
|
|
||||||
TjEbMBkGA1UECgwSQW50IEZpbmFuY2lhbCB0ZXN0MSUwIwYDVQQLDBxDZXJ0aWZpY2F0aW9uIEF1
|
|
||||||
dGhvcml0eSB0ZXN0MT4wPAYDVQQDDDVBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
|
|
||||||
aXR5IENsYXNzIDIgUjEgdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMh4FKYO
|
|
||||||
ZyRQHD6eFbPKZeSAnrfjfU7xmS9Yoozuu+iuqZlb6Z0SPLUqqTZAFZejOcmr07ln/pwZxluqplxC
|
|
||||||
5+B48End4nclDMlT5HPrDr3W0frs6Xsa2ZNcyil/iKNB5MbGll8LRAxntsKvZZj6vUTMb705gYgm
|
|
||||||
VUMILwi/ZxKTQqBtkT/kQQ5y6nOZsj7XI5rYdz6qqOROrpvS/d7iypdHOMIM9Iz9DlL1mrCykbBi
|
|
||||||
t25y+gTeXmuisHUwqaRpwtCGK4BayCqxRGbNipe6W73EK9lBrrzNtTr9NaysesT/v+l25JHCL9tG
|
|
||||||
wpNr1oWFzk4IHVOg0ORiQ6SUgxZUTYcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
|
|
||||||
DQEBCwUAA4IBAQBWThEoIaQoBX2YeRY/I8gu6TYnFXtyuCljANnXnM38ft+ikhE5mMNgKmJYLHvT
|
|
||||||
yWWWgwHoSAWEuml7EGbE/2AK2h3k0MdfiWLzdmpPCRG/RJHk6UB1pMHPilI+c0MVu16OPpKbg5Vf
|
|
||||||
LTv7dsAB40AzKsvyYw88/Ezi1osTXo6QQwda7uefvudirtb8FcQM9R66cJxl3kt1FXbpYwheIm/p
|
|
||||||
j1mq64swCoIYu4NrsUYtn6CV542DTQMI5QdXkn+PzUUly8F6kDp+KpMNd0avfWNL5+O++z+F5Szy
|
|
||||||
1CPta1D7EQ/eYmMP+mOQ35oifWIoFCpN6qQVBS/Hob1J/UUyg7BW
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG
|
|
||||||
EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw
|
|
||||||
MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO
|
|
||||||
UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE
|
|
||||||
MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT
|
|
||||||
V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti
|
|
||||||
W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ
|
|
||||||
MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b
|
|
||||||
53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI
|
|
||||||
pDoiVhsLwg==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIF0zCCA7ugAwIBAgIIH8+hjWpIDREwDQYJKoZIhvcNAQELBQAwejELMAkGA1UE
|
|
||||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmlj
|
|
||||||
YXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmlj
|
|
||||||
YXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMyMTEzNDg0MFoXDTM4MDIyODEzNDg0
|
|
||||||
MFowejELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNV
|
|
||||||
BAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5j
|
|
||||||
aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMIICIjANBgkqhkiG9w0BAQEF
|
|
||||||
AAOCAg8AMIICCgKCAgEAtytTRcBNuur5h8xuxnlKJetT65cHGemGi8oD+beHFPTk
|
|
||||||
rUTlFt9Xn7fAVGo6QSsPb9uGLpUFGEdGmbsQ2q9cV4P89qkH04VzIPwT7AywJdt2
|
|
||||||
xAvMs+MgHFJzOYfL1QkdOOVO7NwKxH8IvlQgFabWomWk2Ei9WfUyxFjVO1LVh0Bp
|
|
||||||
dRBeWLMkdudx0tl3+21t1apnReFNQ5nfX29xeSxIhesaMHDZFViO/DXDNW2BcTs6
|
|
||||||
vSWKyJ4YIIIzStumD8K1xMsoaZBMDxg4itjWFaKRgNuPiIn4kjDY3kC66Sl/6yTl
|
|
||||||
YUz8AybbEsICZzssdZh7jcNb1VRfk79lgAprm/Ktl+mgrU1gaMGP1OE25JCbqli1
|
|
||||||
Pbw/BpPynyP9+XulE+2mxFwTYhKAwpDIDKuYsFUXuo8t261pCovI1CXFzAQM2w7H
|
|
||||||
DtA2nOXSW6q0jGDJ5+WauH+K8ZSvA6x4sFo4u0KNCx0ROTBpLif6GTngqo3sj+98
|
|
||||||
SZiMNLFMQoQkjkdN5Q5g9N6CFZPVZ6QpO0JcIc7S1le/g9z5iBKnifrKxy0TQjtG
|
|
||||||
PsDwc8ubPnRm/F82RReCoyNyx63indpgFfhN7+KxUIQ9cOwwTvemmor0A+ZQamRe
|
|
||||||
9LMuiEfEaWUDK+6O0Gl8lO571uI5onYdN1VIgOmwFbe+D8TcuzVjIZ/zvHrAGUcC
|
|
||||||
AwEAAaNdMFswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFF90
|
|
||||||
tATATwda6uWx2yKjh0GynOEBMB8GA1UdIwQYMBaAFF90tATATwda6uWx2yKjh0Gy
|
|
||||||
nOEBMA0GCSqGSIb3DQEBCwUAA4ICAQCVYaOtqOLIpsrEikE5lb+UARNSFJg6tpkf
|
|
||||||
tJ2U8QF/DejemEHx5IClQu6ajxjtu0Aie4/3UnIXop8nH/Q57l+Wyt9T7N2WPiNq
|
|
||||||
JSlYKYbJpPF8LXbuKYG3BTFTdOVFIeRe2NUyYh/xs6bXGr4WKTXb3qBmzR02FSy3
|
|
||||||
IODQw5Q6zpXj8prYqFHYsOvGCEc1CwJaSaYwRhTkFedJUxiyhyB5GQwoFfExCVHW
|
|
||||||
05ZFCAVYFldCJvUzfzrWubN6wX0DD2dwultgmldOn/W/n8at52mpPNvIdbZb2F41
|
|
||||||
T0YZeoWnCJrYXjq/32oc1cmifIHqySnyMnavi75DxPCdZsCOpSAT4j4lAQRGsfgI
|
|
||||||
kkLPGQieMfNNkMCKh7qjwdXAVtdqhf0RVtFILH3OyEodlk1HYXqX5iE5wlaKzDop
|
|
||||||
PKwf2Q3BErq1xChYGGVS+dEvyXc/2nIBlt7uLWKp4XFjqekKbaGaLJdjYP5b2s7N
|
|
||||||
1dM0MXQ/f8XoXKBkJNzEiM3hfsU6DOREgMc1DIsFKxfuMwX3EkVQM1If8ghb6x5Y
|
|
||||||
jXayv+NLbidOSzk4vl5QwngO/JYFMkoc6i9LNwEaEtR9PhnrdubxmrtM+RjfBm02
|
|
||||||
77q3dSWFESFQ4QxYWew4pHE0DpWbWy/iMIKQ6UZ5RLvB8GEcgt8ON7BBJeMc+Dyi
|
|
||||||
kT9qhqn+lw==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICiDCCAgygAwIBAgIIQX76UsB/30owDAYIKoZIzj0EAwMFADB6MQswCQYDVQQG
|
|
||||||
EwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UECwwXQ2VydGlmaWNh
|
|
||||||
dGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNpYWwgQ2VydGlmaWNh
|
|
||||||
dGlvbiBBdXRob3JpdHkgRTEwHhcNMTkwNDI4MTYyMDQ0WhcNNDkwNDIwMTYyMDQ0
|
|
||||||
WjB6MQswCQYDVQQGEwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UE
|
|
||||||
CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNp
|
|
||||||
YWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRTEwdjAQBgcqhkjOPQIBBgUrgQQA
|
|
||||||
IgNiAASCCRa94QI0vR5Up9Yr9HEupz6hSoyjySYqo7v837KnmjveUIUNiuC9pWAU
|
|
||||||
WP3jwLX3HkzeiNdeg22a0IZPoSUCpasufiLAnfXh6NInLiWBrjLJXDSGaY7vaokt
|
|
||||||
rpZvAdmjXTBbMAsGA1UdDwQEAwIBBjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRZ
|
|
||||||
4ZTgDpksHL2qcpkFkxD2zVd16TAfBgNVHSMEGDAWgBRZ4ZTgDpksHL2qcpkFkxD2
|
|
||||||
zVd16TAMBggqhkjOPQQDAwUAA2gAMGUCMQD4IoqT2hTUn0jt7oXLdMJ8q4vLp6sg
|
|
||||||
wHfPiOr9gxreb+e6Oidwd2LDnC4OUqCWiF8CMAzwKs4SnDJYcMLf2vpkbuVE4dTH
|
|
||||||
Rglz+HGcTLWsFs4KxLsq7MuU+vJTBUeDJeDjdA==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDxTCCAq2gAwIBAgIUEMdk6dVgOEIS2cCP0Q43P90Ps5YwDQYJKoZIhvcNAQEF
|
|
||||||
BQAwajELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM
|
|
||||||
E0NoaW5hIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMMH2lUcnVzQ2hpbmEgQ2xhc3Mg
|
|
||||||
MiBSb290IENBIC0gRzMwHhcNMTMwNDE4MDkzNjU2WhcNMzMwNDE4MDkzNjU2WjBq
|
|
||||||
MQswCQYDVQQGEwJDTjETMBEGA1UECgwKaVRydXNDaGluYTEcMBoGA1UECwwTQ2hp
|
|
||||||
bmEgVHJ1c3QgTmV0d29yazEoMCYGA1UEAwwfaVRydXNDaGluYSBDbGFzcyAyIFJv
|
|
||||||
b3QgQ0EgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPPShpV
|
|
||||||
nJbMqqCw6Bz1kehnoPst9pkr0V9idOwU2oyS47/HjJXk9Rd5a9xfwkPO88trUpz5
|
|
||||||
4GmmwspDXjVFu9L0eFaRuH3KMha1Ak01citbF7cQLJlS7XI+tpkTGHEY5pt3EsQg
|
|
||||||
wykfZl/A1jrnSkspMS997r2Gim54cwz+mTMgDRhZsKK/lbOeBPpWtcFizjXYCqhw
|
|
||||||
WktvQfZBYi6o4sHCshnOswi4yV1p+LuFcQ2ciYdWvULh1eZhLxHbGXyznYHi0dGN
|
|
||||||
z+I9H8aXxqAQfHVhbdHNzi77hCxFjOy+hHrGsyzjrd2swVQ2iUWP8BfEQqGLqM1g
|
|
||||||
KgWKYfcTGdbPB1MCAwEAAaNjMGEwHQYDVR0OBBYEFG/oAMxTVe7y0+408CTAK8hA
|
|
||||||
uTyRMB8GA1UdIwQYMBaAFG/oAMxTVe7y0+408CTAK8hAuTyRMA8GA1UdEwEB/wQF
|
|
||||||
MAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBLnUTfW7hp
|
|
||||||
emMbuUGCk7RBswzOT83bDM6824EkUnf+X0iKS95SUNGeeSWK2o/3ALJo5hi7GZr3
|
|
||||||
U8eLaWAcYizfO99UXMRBPw5PRR+gXGEronGUugLpxsjuynoLQu8GQAeysSXKbN1I
|
|
||||||
UugDo9u8igJORYA+5ms0s5sCUySqbQ2R5z/GoceyI9LdxIVa1RjVX8pYOj8JFwtn
|
|
||||||
DJN3ftSFvNMYwRuILKuqUYSHc2GPYiHVflDh5nDymCMOQFcFG3WsEuB+EYQPFgIU
|
|
||||||
1DHmdZcz7Llx8UOZXX2JupWCYzK1XhJb+r4hK5ncf/w8qGtYlmyJpxk3hr1TfUJX
|
|
||||||
Yf4Zr0fJsGuv
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDmTCCAoGgAwIBAgIQICMRB2LW76yahgdg3IFNPDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
|
|
||||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
|
|
||||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
|
|
||||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDU0NjE5WhcNMjQxMTExMDU0NjE5WjBr
|
|
||||||
MQswCQYDVQQGEwJDTjEfMB0GA1UECgwWbWJvbmZ5OTAxNUBzYW5kYm94LmNvbTEPMA0GA1UECwwG
|
|
||||||
QWxpcGF5MSowKAYDVQQDDCEyMDg4NzIxMDIwNzUwNTgxLTkwMjEwMDAxMzE2NTgwMjMwggEiMA0G
|
|
||||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxihQPf1Q+g9ArgM46shVqL5sbRha/df95D1PsWyEq
|
|
||||||
ANmWmG4zZ+ksYDVQrc4KzhSRoi56sm/7TDFYTmM6bW99e/nKW58WxyZB4ie5qA3F4n17psPyDqb8
|
|
||||||
IokcQmCphSFDaXQD6AoXoLNtTM0vAI2cWxAgebZ/vsrdj5Ntjt+Rp3NYMCk1i5xovHcfILzLEGbX
|
|
||||||
QXoT9fo5AhHotTWa6xHVLPUGY9qwLzQxHzBmvy5ZMfnOfJkm/mDisTSqAUB59F3dzU/1ARVkEZ1w
|
|
||||||
Mgb4XohWBw6iurQfbMnH2mIomAAwwZVFv+sXDbL9yMbSMo/SjVsTQprn0Q0EnwLo7nmmOM6HAgMB
|
|
||||||
AAGjEjAQMA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEAn3Y4/C1h9R6ONsBqX3/q
|
|
||||||
XfHX7yX1FM0Y1x48X3/Yxk6HivAkTukhhhVYVKJsbrbzRqHDp9vhAP/FR6o6pAevaYMmLov0VMXU
|
|
||||||
7oAuetgkaYEYkDuNen5/Hpdhqi2vTtdT+q9w8zHJd6MDQ0aoHgIxpLKw5vof2R1N4fwSgNXMiXE5
|
|
||||||
kmllKQMem/+on2p+Sj80/2asxryHIGlH87qPzkffv+kIOkZthbTApTFLLjdVri2QHGe8/cc4xy01
|
|
||||||
/9iR3IUzNahotT41lJ4bMevBY7XMAS3n5ekyABN/9ZRJqhWdXgmFCRN/u56qd6lDgu7R2M2QUoyc
|
|
||||||
LuW5DfgRItKlmUB7sw==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
MIIEpQIBAAKCAQEAsYoUD39UPoPQK4DOOrIVai+bG0YWv3X/eQ9T7FshKgDZlphuM2fpLGA1UK3OCs4UkaIuerJv+0wxWE5jOm1vfXv5ylufFscmQeInuagNxeJ9e6bD8g6m/CKJHEJgqYUhQ2l0A+gKF6CzbUzNLwCNnFsQIHm2f77K3Y+TbY7fkadzWDApNYucaLx3HyC8yxBm10F6E/X6OQIR6LU1musR1Sz1BmPasC80MR8wZr8uWTH5znyZJv5g4rE0qgFAefRd3c1P9QEVZBGdcDIG+F6IVgcOorq0H2zJx9piKJgAMMGVRb/rFw2y/cjG0jKP0o1bE0Ka59ENBJ8C6O55pjjOhwIDAQABAoIBAFetNfz1R7hbxjlFshMAkVzQR8wvT9qbvl+dtzdZRcaFhu89NecDIP7+QDYor0FcxoGpU0TazDyRQyk2BQD8vHt+9zv9BVLtZLJSqoWgPbUFBi1DjS8EF2ka8RVYnn35NhUhhd7L//ftL88Bh673mfembQ9srDjoEy1Z01feoABAnCMkNFl986DmEwnarvEufXSDIgeN4ioMxha4NvfIPuI0zpVdV1O9sv+SGC+VEWZBtN3GNsaf4zS/f8FVGvTiU/Abz0gSw/iwSPHclDWQDTN3yFHf/tfqlzh0mH0WfhnuOBFWXzK+R7fbnM+asI9ttvzRcfpzgRGXdPcNcOv/6cECgYEA3DVqpi1k8MYfJixju6SG5gfyhM4VFksFmCMaNPgtatDMBKLMTgV/Ej6LXREojcy29uZl83F09pVlpd41eG39ULIPktixA/BqErQ2UaWh6kOxifycpu22Jh0r09hax6UgVrcBrrnCJEjcFsuJlrZvXQSzc3PBxjWy5gjabS5h9iECgYEAzmVAIh2frF01Y95zsLueAhhZwCtPanm6kf7ivR4r1plIX3b2sNRhWGmEHFgaCE6Braa0ogQ73Hd26kw4ZW+D6QMGC/zjCBEzDLLf++SjdVUHiY5AR4WHqXzq1jdAlsVyo9R661oAOp3lhiJVGLNXkHyEfEVPHsaxJh4osYSbX6cCgYEAx32Qx0i6eDFTyLZQB46uMrgiaVN04QRH5iJuvGvUYT8UhGKjaU8rZfDJOh+wOH2rhxMEaz1uc3C2bERY9mfWI4Ob/jFWc7YZsiYWS3Mcsuhubw4tMECLUg39RWZsHw8ls8kIuixIh6yFzhTH6YQOcRswIrhMZG8DScfdcSmiz2ECgYEAkWP1t5KSpkLKl11etcKUXfl1T8+yk9jIOowIgRw92WAFAWq2AH67TCKYM7dEL1HOO9tRJ0hAOt/U3ttuZtYVYBEHM26jJ02mXm2rJrA7DS4mrxmL4lYH6LbcXqZxU0Qnq4zEQgIWYzRTORf6Rfof1uJAGaJhR9bDd4yLMfGt2cUCgYEAo216Y61xOHUTA4AF1eekk+r+uOcQgQDvLXfs9FkDdJLk0mPG48/+eIYpPFnANJ/riF/DWOp8WGEe2IzA9yUFexzDbNQK8ha9kGcxaSAyiCwzjZ/t9/+hScDSV8kNqWSRSisu/YOFleEHbokT6mbLZ+gdqES8mUUanaEBzRQYGxo=
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -20,9 +20,9 @@ type CaptchaService struct {
|
|||||||
client *req.Client
|
client *req.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCaptchaService(captchaConfig types.CaptchaConfig) *CaptchaService {
|
func NewCaptchaService(sysConfig *types.SystemConfig) *CaptchaService {
|
||||||
return &CaptchaService{
|
return &CaptchaService{
|
||||||
config: captchaConfig,
|
config: sysConfig.Captcha,
|
||||||
client: req.C().SetTimeout(10 * time.Second),
|
client: req.C().SetTimeout(10 * time.Second),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ func (s *CaptchaService) GetConfig() types.CaptchaConfig {
|
|||||||
return s.config
|
return s.config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CaptchaService) Get() (interface{}, error) {
|
func (s *CaptchaService) Get() (any, error) {
|
||||||
url := fmt.Sprintf("%s/api/captcha/get", types.GeekAPIURL)
|
url := fmt.Sprintf("%s/api/captcha/get", types.GeekAPIURL)
|
||||||
var res types.BizVo
|
var res types.BizVo
|
||||||
r, err := s.client.R().
|
r, err := s.client.R().
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
"geekai/store"
|
"geekai/store"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/utils"
|
"geekai/utils"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
@@ -98,10 +97,8 @@ type imgReq struct {
|
|||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Image []string `json:"image,omitempty"`
|
Image []string `json:"image,omitempty"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
N int `json:"n,omitempty"`
|
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||||
Size string `json:"size,omitempty"`
|
Size string `json:"size,omitempty"`
|
||||||
Quality string `json:"quality,omitempty"`
|
|
||||||
Style string `json:"style,omitempty"`
|
|
||||||
ResponseFormat string `json:"response_format,omitempty"`
|
ResponseFormat string `json:"response_format,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,10 +113,10 @@ type imgRes struct {
|
|||||||
|
|
||||||
type ErrRes struct {
|
type ErrRes struct {
|
||||||
Error struct {
|
Error struct {
|
||||||
Code interface{} `json:"code"`
|
Code any `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Param interface{} `json:"param"`
|
Param any `json:"param"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
} `json:"error"`
|
} `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +127,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
|||||||
if task.ModelId > 0 {
|
if task.ModelId > 0 {
|
||||||
s.db.Where("id", task.ModelId).First(&chatModel)
|
s.db.Where("id", task.ModelId).First(&chatModel)
|
||||||
} else {
|
} else {
|
||||||
s.db.Where("value", task.ModelName).First(&chatModel)
|
s.db.Where("value", task.ModelValue).First(&chatModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get image generation API KEY
|
// get image generation API KEY
|
||||||
@@ -153,16 +150,16 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
|||||||
}
|
}
|
||||||
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
|
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
|
||||||
reqBody := imgReq{
|
reqBody := imgReq{
|
||||||
Model: chatModel.Value,
|
Model: chatModel.Value,
|
||||||
Prompt: task.Prompt,
|
Prompt: task.Prompt,
|
||||||
N: 1,
|
AspectRatio: task.AspectRatio,
|
||||||
Size: task.Size,
|
Size: task.Size,
|
||||||
Style: task.Style,
|
ResponseFormat: "url",
|
||||||
Quality: task.Quality,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图片编辑
|
// 图片编辑
|
||||||
if len(task.Image) > 0 {
|
if len(task.Image) > 0 {
|
||||||
reqBody.Prompt = fmt.Sprintf("%s, %s", strings.Join(task.Image, " "), task.Prompt)
|
reqBody.Image = task.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
|
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
|
||||||
@@ -185,7 +182,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
|||||||
// update the api key last use time
|
// update the api key last use time
|
||||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||||
var imgURL string
|
var imgURL string
|
||||||
var data = map[string]interface{}{
|
var data = map[string]any{
|
||||||
"progress": 100,
|
"progress": 100,
|
||||||
"prompt": task.Prompt,
|
"prompt": task.Prompt,
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
package jimeng
|
package jimeng
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/volcengine/volc-sdk-golang/base"
|
"github.com/volcengine/volc-sdk-golang/base"
|
||||||
"github.com/volcengine/volc-sdk-golang/service/visual"
|
"github.com/volcengine/volc-sdk-golang/service/visual"
|
||||||
|
"github.com/volcengine/volcengine-go-sdk/service/arkruntime"
|
||||||
|
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
|
||||||
|
"github.com/volcengine/volcengine-go-sdk/volcengine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client 即梦API客户端
|
// Client 即梦API客户端
|
||||||
@@ -50,6 +56,22 @@ func (c *Client) UpdateConfig(config types.JimengConfig) error {
|
|||||||
"Version": []string{"2022-08-31"},
|
"Version": []string{"2022-08-31"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"CVSubmitTask": {
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/",
|
||||||
|
Query: url.Values{
|
||||||
|
"Action": []string{"CVSubmitTask"},
|
||||||
|
"Version": []string{"2022-08-31"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"CVGetResult": {
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/",
|
||||||
|
Query: url.Values{
|
||||||
|
"Action": []string{"CVGetResult"},
|
||||||
|
"Version": []string{"2022-08-31"},
|
||||||
|
},
|
||||||
|
},
|
||||||
"CVProcess": {
|
"CVProcess": {
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
@@ -71,6 +93,22 @@ func (c *Client) UpdateConfig(config types.JimengConfig) error {
|
|||||||
return c.testConnection()
|
return c.testConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetErrorMessage 根据错误代码获取对应的错误信息
|
||||||
|
func GetErrorMessage(code int) string {
|
||||||
|
if message, exists := errorCodeMessages[code]; exists {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("未知错误代码: %d", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleResponseError 处理响应错误,根据错误代码返回详细的错误信息
|
||||||
|
func HandleResponseError(code int, message string) error {
|
||||||
|
if code == ECSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New(GetErrorMessage(code))
|
||||||
|
}
|
||||||
|
|
||||||
// testConnection 测试即梦AI连接
|
// testConnection 测试即梦AI连接
|
||||||
func (c *Client) testConnection() error {
|
func (c *Client) testConnection() error {
|
||||||
|
|
||||||
@@ -80,7 +118,7 @@ func (c *Client) testConnection() error {
|
|||||||
TaskId: "test_task_id_12345",
|
TaskId: "test_task_id_12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := c.QueryTask(testReq)
|
_, err := c.QueryTask(testReq, ASyncActionGetResult)
|
||||||
// 即使任务不存在,只要不是认证错误就说明连接正常
|
// 即使任务不存在,只要不是认证错误就说明连接正常
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 检查是否是认证错误
|
// 检查是否是认证错误
|
||||||
@@ -94,7 +132,7 @@ func (c *Client) testConnection() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SubmitTask 提交异步任务
|
// SubmitTask 提交异步任务
|
||||||
func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error) {
|
func (c *Client) SubmitTask(req map[string]any) (*SubmitTaskResponse, error) {
|
||||||
// 直接将请求转为map[string]interface{}
|
// 直接将请求转为map[string]interface{}
|
||||||
reqBodyBytes, err := json.Marshal(req)
|
reqBodyBytes, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -103,9 +141,14 @@ func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error)
|
|||||||
|
|
||||||
// 直接使用序列化后的字节
|
// 直接使用序列化后的字节
|
||||||
jsonBody := reqBodyBytes
|
jsonBody := reqBodyBytes
|
||||||
|
action := ASyncActionSubmit
|
||||||
|
if v, ok := req["action"]; ok {
|
||||||
|
action = v.(string)
|
||||||
|
delete(req, "action")
|
||||||
|
}
|
||||||
|
|
||||||
// 调用SDK的JSON方法
|
// 调用SDK的JSON方法
|
||||||
respBody, statusCode, err := c.visual.Client.Json("CVSync2AsyncSubmitTask", nil, string(jsonBody))
|
respBody, statusCode, err := c.visual.Client.Json(action, nil, string(jsonBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
|
return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
|
||||||
}
|
}
|
||||||
@@ -118,11 +161,70 @@ func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error)
|
|||||||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查响应错误代码
|
||||||
|
if err := HandleResponseError(result.Code, result.Message); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 识别数字人主体
|
||||||
|
func (c *Client) AvatarRecognition(imgUrl string, reqKey string) error {
|
||||||
|
params := map[string]any{
|
||||||
|
"image_url": imgUrl,
|
||||||
|
"req_key": reqKey,
|
||||||
|
}
|
||||||
|
reqBodyBytes, err := json.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal request failed: %w", err)
|
||||||
|
}
|
||||||
|
// 调用SDK的JSON方法
|
||||||
|
respBody, statusCode, err := c.visual.Client.Json(SyncActionSubmit, nil, string(reqBodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var result SubmitTaskResponse
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查响应错误代码
|
||||||
|
if err := HandleResponseError(result.Code, result.Message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待任务完成
|
||||||
|
for {
|
||||||
|
resp, err := c.QueryTask(&QueryTaskRequest{
|
||||||
|
ReqKey: reqKey,
|
||||||
|
TaskId: result.Data.TaskId,
|
||||||
|
}, SyncActionGetResult)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("query task failed: %w", err)
|
||||||
|
}
|
||||||
|
if resp.Data.Status != types.JMTaskStatusDone {
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var respData map[string]int
|
||||||
|
if err := json.Unmarshal([]byte(resp.Data.RespData), &respData); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal response failed: %w", err)
|
||||||
|
}
|
||||||
|
logger.Debugf("Jimeng AvatarRecognition Response: %+v", resp)
|
||||||
|
if respData["status"] == 1 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("不包含人、类人、拟人等主体")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// QueryTask 查询任务结果
|
// QueryTask 查询任务结果
|
||||||
func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
|
func (c *Client) QueryTask(req *QueryTaskRequest, action string) (*QueryTaskResponse, error) {
|
||||||
// 序列化请求
|
// 序列化请求
|
||||||
jsonBody, err := json.Marshal(req)
|
jsonBody, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -130,7 +232,7 @@ func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调用SDK的JSON方法
|
// 调用SDK的JSON方法
|
||||||
respBody, statusCode, err := c.visual.Client.Json("CVSync2AsyncGetResult", nil, string(jsonBody))
|
respBody, statusCode, err := c.visual.Client.Json(action, nil, string(jsonBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err)
|
return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err)
|
||||||
}
|
}
|
||||||
@@ -143,30 +245,37 @@ func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
|
|||||||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
// 检查响应错误代码
|
||||||
}
|
if err := HandleResponseError(result.Code, result.Message); err != nil {
|
||||||
|
return nil, err
|
||||||
// SubmitSyncTask 提交同步任务(仅用于文生图)
|
|
||||||
func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*QueryTaskResponse, error) {
|
|
||||||
// 序列化请求
|
|
||||||
jsonBody, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("marshal request failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用SDK的JSON方法
|
|
||||||
respBody, statusCode, err := c.visual.Client.Json("CVProcess", nil, string(jsonBody))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("submit sync task failed (status: %d): %w", statusCode, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("Jimeng SubmitSyncTask Response: %s", string(respBody))
|
|
||||||
|
|
||||||
// 解析响应,同步任务直接返回结果
|
|
||||||
var result QueryTaskResponse
|
|
||||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubmitSyncImageTask 提交同步生图任务
|
||||||
|
func (c *Client) SubmitSyncImageTask(req types.JimengTaskRequest) (*model.ImagesResponse, error) {
|
||||||
|
// 配置火山引擎访问密钥,目前只支持API Key验证
|
||||||
|
client := arkruntime.NewClientWithApiKey(c.config.ApiKey)
|
||||||
|
// 构造生图请求
|
||||||
|
sequentialImageGeneration := model.SequentialImageGeneration("disabled")
|
||||||
|
generateReq := model.GenerateImagesRequest{
|
||||||
|
Model: req.ReqKey, // 模型名称
|
||||||
|
Prompt: req.Prompt, // 提示词
|
||||||
|
Size: volcengine.String(req.Size), // 图片尺寸
|
||||||
|
SequentialImageGeneration: &sequentialImageGeneration, // 禁用序列生成
|
||||||
|
ResponseFormat: volcengine.String(model.GenerateImagesResponseFormatURL), // 响应格式为 URL
|
||||||
|
Watermark: volcengine.Bool(false), // 不添加水印
|
||||||
|
OptimizePrompt: volcengine.Bool(true), // 优化提示词
|
||||||
|
}
|
||||||
|
if len(req.ImageUrls) > 0 {
|
||||||
|
generateReq.Image = req.ImageUrls
|
||||||
|
}
|
||||||
|
// 调用生图 API
|
||||||
|
resp, err := client.GenerateImages(context.Background(), generateReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"geekai/core/types"
|
||||||
logger2 "geekai/logger"
|
logger2 "geekai/logger"
|
||||||
|
"geekai/service"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/store"
|
"geekai/store"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
@@ -22,29 +24,31 @@ var logger = logger2.GetLogger()
|
|||||||
|
|
||||||
// Service 即梦服务(合并了消费者功能)
|
// Service 即梦服务(合并了消费者功能)
|
||||||
type Service struct {
|
type Service struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
taskQueue *store.RedisQueue
|
taskQueue *store.RedisQueue
|
||||||
client *Client
|
client *Client
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
running bool
|
running bool
|
||||||
uploader *oss.UploaderManager
|
uploader *oss.UploaderManager
|
||||||
|
userService *service.UserService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService 创建即梦服务
|
// NewService 创建即梦服务
|
||||||
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client) *Service {
|
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client, userService *service.UserService) *Service {
|
||||||
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
|
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &Service{
|
return &Service{
|
||||||
db: db,
|
db: db,
|
||||||
redis: redisCli,
|
redis: redisCli,
|
||||||
taskQueue: taskQueue,
|
taskQueue: taskQueue,
|
||||||
client: client,
|
client: client,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
running: false,
|
running: false,
|
||||||
uploader: uploader,
|
uploader: uploader,
|
||||||
|
userService: userService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,35 +99,29 @@ func (s *Service) processNextTask() {
|
|||||||
|
|
||||||
if err := s.ProcessTask(jobId); err != nil {
|
if err := s.ProcessTask(jobId); err != nil {
|
||||||
logger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err)
|
logger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err)
|
||||||
s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, err.Error())
|
s.UpdateJobStatus(jobId, types.JMTaskStatusFailed, err.Error())
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("Jimeng task processed successfully: job_id=%d", jobId)
|
logger.Infof("Jimeng task processed successfully: job_id=%d", jobId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTask 创建任务
|
// CreateTask 创建任务
|
||||||
func (s *Service) CreateTask(userId uint, req *CreateTaskRequest) (*model.JimengJob, error) {
|
func (s *Service) CreateTask(userId uint, req *types.JimengTaskRequest) (*model.JimengJob, error) {
|
||||||
// 生成任务ID
|
// 生成任务ID
|
||||||
taskId := utils.RandString(20)
|
taskId := utils.RandString(20)
|
||||||
|
|
||||||
// 序列化任务参数
|
|
||||||
paramsJson, err := json.Marshal(req.Params)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("marshal task params failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建任务记录
|
// 创建任务记录
|
||||||
job := &model.JimengJob{
|
job := &model.JimengJob{
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
TaskId: taskId,
|
TaskId: taskId,
|
||||||
Type: req.Type,
|
Type: req.TaskType,
|
||||||
ReqKey: req.ReqKey,
|
ReqKey: req.ReqKey,
|
||||||
Prompt: req.Prompt,
|
Prompt: req.Prompt,
|
||||||
TaskParams: string(paramsJson),
|
Params: utils.JsonEncode(req),
|
||||||
Status: model.JMTaskStatusInQueue,
|
Status: types.JMTaskStatusSubmited,
|
||||||
Power: req.Power,
|
Power: req.Power,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到数据库
|
// 保存到数据库
|
||||||
@@ -148,25 +146,71 @@ func (s *Service) ProcessTask(jobId uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新任务状态为处理中
|
// 更新任务状态为处理中
|
||||||
if err := s.UpdateJobStatus(job.Id, model.JMTaskStatusGenerating, ""); err != nil {
|
if err := s.UpdateJobStatus(job.Id, types.JMTaskStatusGenerating, ""); err != nil {
|
||||||
return fmt.Errorf("update job status failed: %w", err)
|
return fmt.Errorf("update job status failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析任务参数
|
||||||
|
var req types.JimengTaskRequest
|
||||||
|
err := utils.JsonDecode(job.Params, &req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse task params failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 构建请求并提交任务
|
// 构建请求并提交任务
|
||||||
req, err := s.buildTaskRequest(&job)
|
params, err := s.buildTaskRequest(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s.handleTaskError(job.Id, fmt.Sprintf("build task request failed: %v", err))
|
return s.handleTaskError(job.Id, fmt.Sprintf("build task request failed: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("提交即梦任务: %+v", req)
|
// 数字人任务,先识别主体
|
||||||
|
if req.TaskType == types.JMTaskTypeVirtualHuman {
|
||||||
|
if err := s.client.AvatarRecognition(req.ImageUrls[0], req.RecognizeKey); err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("avatar recognition failed: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 提交异步任务
|
// 同步任务 ,后台执行
|
||||||
resp, err := s.client.SubmitTask(req)
|
if req.ReqKey == DoubaoSeedream40ReqKey {
|
||||||
|
go func() {
|
||||||
|
resp, err := s.client.SubmitSyncImageTask(req)
|
||||||
|
if err != nil {
|
||||||
|
_ = s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Infof("同步任务提交成功: %+v", resp)
|
||||||
|
// 更新原始数据
|
||||||
|
rawData, _ := json.Marshal(resp)
|
||||||
|
updates := map[string]any{
|
||||||
|
"raw_data": string(rawData),
|
||||||
|
}
|
||||||
|
if resp.Error != nil {
|
||||||
|
updates["status"] = types.JMTaskStatusFailed
|
||||||
|
updates["err_msg"] = resp.Error.Message
|
||||||
|
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务状态
|
||||||
|
updates["status"] = types.JMTaskStatusSuccess
|
||||||
|
// 下载图片
|
||||||
|
imgUrl, err := s.uploader.GetUploadHandler().PutUrlFile(*resp.Data[0].Url, ".png", false)
|
||||||
|
if err == nil {
|
||||||
|
updates["img_url"] = imgUrl
|
||||||
|
}
|
||||||
|
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("提交即梦任务: %+v", params)
|
||||||
|
// 异步任务 ,前台执行
|
||||||
|
resp, err := s.client.SubmitTask(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Code != 10000 {
|
if resp.Code != CodeSuccess {
|
||||||
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +219,7 @@ func (s *Service) ProcessTask(jobId uint) error {
|
|||||||
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||||
"task_id": resp.Data.TaskId,
|
"task_id": resp.Data.TaskId,
|
||||||
"raw_data": string(rawData),
|
"raw_data": string(rawData),
|
||||||
|
"status": types.JMTaskStatusInQueue, // 把任务状态改成排队中,以便开启轮询
|
||||||
"updated_at": time.Now(),
|
"updated_at": time.Now(),
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
logger.Errorf("update jimeng job task_id failed: %v", err)
|
logger.Errorf("update jimeng job task_id failed: %v", err)
|
||||||
@@ -184,172 +229,51 @@ func (s *Service) ProcessTask(jobId uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildTaskRequest 构建任务请求(统一的参数解析)
|
// buildTaskRequest 构建任务请求(统一的参数解析)
|
||||||
func (s *Service) buildTaskRequest(job *model.JimengJob) (*SubmitTaskRequest, error) {
|
func (s *Service) buildTaskRequest(req *types.JimengTaskRequest) (map[string]any, error) {
|
||||||
// 解析任务参数
|
|
||||||
var params map[string]any
|
var params map[string]any
|
||||||
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
err := utils.JsonDecode(utils.JsonEncode(req), ¶ms)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse task params failed: %w", err)
|
return nil, fmt.Errorf("parse task params failed: %w", err)
|
||||||
}
|
}
|
||||||
|
// 把 size 转成 width 和 height
|
||||||
// 构建基础请求
|
if size, ok := params["size"]; ok {
|
||||||
req := &SubmitTaskRequest{
|
if sizeStr, ok := size.(string); ok {
|
||||||
ReqKey: job.ReqKey,
|
if strings.Contains(sizeStr, "x") {
|
||||||
Prompt: job.Prompt,
|
sizes := strings.Split(sizeStr, "x")
|
||||||
}
|
params["width"] = sizes[0]
|
||||||
|
params["height"] = sizes[1]
|
||||||
// 根据任务类型设置特定参数
|
|
||||||
switch job.Type {
|
|
||||||
case model.JMTaskTypeTextToImage:
|
|
||||||
s.setTextToImageParams(req, params)
|
|
||||||
case model.JMTaskTypeImageToImage:
|
|
||||||
s.setImageToImageParams(req, params)
|
|
||||||
case model.JMTaskTypeImageEdit:
|
|
||||||
s.setImageEditParams(req, params)
|
|
||||||
case model.JMTaskTypeImageEffects:
|
|
||||||
s.setImageEffectsParams(req, params)
|
|
||||||
case model.JMTaskTypeTextToVideo:
|
|
||||||
s.setTextToVideoParams(req, params)
|
|
||||||
case model.JMTaskTypeImageToVideo:
|
|
||||||
s.setImageToVideoParams(req, params)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported task type: %s", job.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setTextToImageParams 设置文生图参数
|
|
||||||
func (s *Service) setTextToImageParams(req *SubmitTaskRequest, params map[string]any) {
|
|
||||||
if seed, ok := params["seed"]; ok {
|
|
||||||
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
|
||||||
req.Seed = seedVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if scale, ok := params["scale"]; ok {
|
|
||||||
if scaleVal, ok := scale.(float64); ok {
|
|
||||||
req.Scale = scaleVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if width, ok := params["width"]; ok {
|
|
||||||
if widthVal, ok := width.(float64); ok {
|
|
||||||
req.Width = int(widthVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if height, ok := params["height"]; ok {
|
|
||||||
if heightVal, ok := height.(float64); ok {
|
|
||||||
req.Height = int(heightVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if usePreLlm, ok := params["use_pre_llm"]; ok {
|
|
||||||
if usePreLlmVal, ok := usePreLlm.(bool); ok {
|
|
||||||
req.UsePreLLM = usePreLlmVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setImageToImageParams 设置图生图参数
|
|
||||||
func (s *Service) setImageToImageParams(req *SubmitTaskRequest, params map[string]any) {
|
|
||||||
if imageInput, ok := params["image_input"].(string); ok {
|
|
||||||
req.ImageInput = imageInput
|
|
||||||
}
|
|
||||||
if gpen, ok := params["gpen"]; ok {
|
|
||||||
if gpenVal, ok := gpen.(float64); ok {
|
|
||||||
req.Gpen = gpenVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if skin, ok := params["skin"]; ok {
|
|
||||||
if skinVal, ok := skin.(float64); ok {
|
|
||||||
req.Skin = skinVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if skinUnifi, ok := params["skin_unifi"]; ok {
|
|
||||||
if skinUnifiVal, ok := skinUnifi.(float64); ok {
|
|
||||||
req.SkinUnifi = skinUnifiVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if genMode, ok := params["gen_mode"].(string); ok {
|
|
||||||
req.GenMode = genMode
|
|
||||||
}
|
|
||||||
s.setCommonParams(req, params) // 复用通用参数
|
|
||||||
}
|
|
||||||
|
|
||||||
// setImageEditParams 设置图像编辑参数
|
|
||||||
func (s *Service) setImageEditParams(req *SubmitTaskRequest, params map[string]any) {
|
|
||||||
if imageUrls, ok := params["image_urls"].([]any); ok {
|
|
||||||
for _, url := range imageUrls {
|
|
||||||
if urlStr, ok := url.(string); ok {
|
|
||||||
req.ImageUrls = append(req.ImageUrls, urlStr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
delete(params, "size")
|
||||||
}
|
}
|
||||||
if binaryData, ok := params["binary_data_base64"].([]any); ok {
|
|
||||||
for _, data := range binaryData {
|
|
||||||
if dataStr, ok := data.(string); ok {
|
|
||||||
req.BinaryDataBase64 = append(req.BinaryDataBase64, dataStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if scale, ok := params["scale"]; ok {
|
|
||||||
if scaleVal, ok := scale.(float64); ok {
|
|
||||||
req.Scale = scaleVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.setCommonParams(req, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setImageEffectsParams 设置图像特效参数
|
// duration 转成 frames
|
||||||
func (s *Service) setImageEffectsParams(req *SubmitTaskRequest, params map[string]any) {
|
if duration, ok := params["duration"]; ok {
|
||||||
if imageInput1, ok := params["image_input1"].(string); ok {
|
if secs, ok := duration.(int); ok {
|
||||||
req.ImageInput1 = imageInput1
|
params["frames"] = secs*24 + 1
|
||||||
}
|
|
||||||
if templateId, ok := params["template_id"].(string); ok {
|
|
||||||
req.TemplateId = templateId
|
|
||||||
}
|
|
||||||
if width, ok := params["width"]; ok {
|
|
||||||
if widthVal, ok := width.(float64); ok {
|
|
||||||
req.Width = int(widthVal)
|
|
||||||
}
|
}
|
||||||
|
delete(params, "duration")
|
||||||
}
|
}
|
||||||
if height, ok := params["height"]; ok {
|
|
||||||
if heightVal, ok := height.(float64); ok {
|
|
||||||
req.Height = int(heightVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setTextToVideoParams 设置文生视频参数
|
// 单独处理图片特效任务
|
||||||
func (s *Service) setTextToVideoParams(req *SubmitTaskRequest, params map[string]any) {
|
if req.ReqKey == ImageEffectReqKey {
|
||||||
if aspectRatio, ok := params["aspect_ratio"].(string); ok {
|
params["image_input1"] = req.ImageUrls[0]
|
||||||
req.AspectRatio = aspectRatio
|
delete(params, "image_urls")
|
||||||
}
|
}
|
||||||
s.setCommonParams(req, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setImageToVideoParams 设置图生视频参数
|
// 动作迁移,数字人任务参数处理
|
||||||
func (s *Service) setImageToVideoParams(req *SubmitTaskRequest, params map[string]any) {
|
if req.TaskType == types.JMTaskTypeVirtualHuman || req.TaskType == types.JMTaskTypeActionTransfer {
|
||||||
s.setImageEditParams(req, params) // 复用图像编辑的参数设置
|
params["image_url"] = req.ImageUrls[0]
|
||||||
if aspectRatio, ok := params["aspect_ratio"].(string); ok {
|
delete(params, "image_urls")
|
||||||
req.AspectRatio = aspectRatio
|
}
|
||||||
|
if req.RecognizeKey != "" {
|
||||||
|
delete(params, "recognize_key")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// setCommonParams 设置通用参数(seed, width, height等)
|
// 删除多余参数,剩下的就是各个任务自己专有参数了
|
||||||
func (s *Service) setCommonParams(req *SubmitTaskRequest, params map[string]any) {
|
delete(params, "type")
|
||||||
if seed, ok := params["seed"]; ok {
|
delete(params, "power")
|
||||||
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
return params, nil
|
||||||
req.Seed = seedVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if width, ok := params["width"]; ok {
|
|
||||||
if widthVal, ok := width.(float64); ok {
|
|
||||||
req.Width = int(widthVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if height, ok := params["height"]; ok {
|
|
||||||
if heightVal, ok := height.(float64); ok {
|
|
||||||
req.Height = int(heightVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pollTaskStatus 轮询任务状态
|
// pollTaskStatus 轮询任务状态
|
||||||
@@ -357,12 +281,10 @@ func (s *Service) pollTaskStatus() {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
var jobs []model.JimengJob
|
var jobs []model.JimengJob
|
||||||
s.db.Where("status IN (?)", []model.JMTaskStatus{model.JMTaskStatusGenerating, model.JMTaskStatusInQueue}).Find(&jobs)
|
// 找出排队中和处理中的任务进行轮询
|
||||||
if len(jobs) == 0 {
|
s.db.Where("status IN (?)", []types.JMTaskStatus{
|
||||||
logger.Debugf("no jimeng task to poll, sleep 10s")
|
types.JMTaskStatusGenerating,
|
||||||
time.Sleep(10 * time.Second)
|
types.JMTaskStatusInQueue}).Find(&jobs)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
// 任务超时处理
|
// 任务超时处理
|
||||||
@@ -371,12 +293,17 @@ func (s *Service) pollTaskStatus() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 豆包生图 4.0 是同步任务,不需要轮询
|
||||||
|
if job.ReqKey == DoubaoSeedream40ReqKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// 查询任务状态
|
// 查询任务状态
|
||||||
resp, err := s.client.QueryTask(&QueryTaskRequest{
|
resp, err := s.client.QueryTask(&QueryTaskRequest{
|
||||||
ReqKey: job.ReqKey,
|
ReqKey: job.ReqKey,
|
||||||
TaskId: job.TaskId,
|
TaskId: job.TaskId,
|
||||||
ReqJson: `{"return_url":true}`,
|
ReqJson: `{"return_url":true}`,
|
||||||
})
|
}, ASyncActionGetResult)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", err.Error()))
|
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", err.Error()))
|
||||||
@@ -387,13 +314,13 @@ func (s *Service) pollTaskStatus() {
|
|||||||
rawData, _ := json.Marshal(resp)
|
rawData, _ := json.Marshal(resp)
|
||||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Update("raw_data", string(rawData))
|
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Update("raw_data", string(rawData))
|
||||||
|
|
||||||
if resp.Code != 10000 {
|
if resp.Code != CodeSuccess {
|
||||||
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", resp.Message))
|
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", resp.Message))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch resp.Data.Status {
|
switch resp.Data.Status {
|
||||||
case model.JMTaskStatusDone:
|
case types.JMTaskStatusDone:
|
||||||
// 判断任务是否成功
|
// 判断任务是否成功
|
||||||
if resp.Message != "Success" {
|
if resp.Message != "Success" {
|
||||||
s.handleTaskError(job.Id, fmt.Sprintf("task failed: %s", resp.Data.AlgorithmBaseResp.StatusMessage))
|
s.handleTaskError(job.Id, fmt.Sprintf("task failed: %s", resp.Data.AlgorithmBaseResp.StatusMessage))
|
||||||
@@ -402,7 +329,7 @@ func (s *Service) pollTaskStatus() {
|
|||||||
|
|
||||||
// 任务完成,更新结果
|
// 任务完成,更新结果
|
||||||
updates := map[string]any{
|
updates := map[string]any{
|
||||||
"status": model.JMTaskStatusSuccess,
|
"status": types.JMTaskStatusSuccess,
|
||||||
"updated_at": time.Now(),
|
"updated_at": time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,15 +352,15 @@ func (s *Service) pollTaskStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
||||||
case model.JMTaskStatusInQueue, model.JMTaskStatusGenerating:
|
case types.JMTaskStatusInQueue, types.JMTaskStatusGenerating:
|
||||||
// 任务处理中
|
// 任务处理中
|
||||||
s.UpdateJobStatus(job.Id, model.JMTaskStatusGenerating, "")
|
s.UpdateJobStatus(job.Id, types.JMTaskStatusGenerating, "")
|
||||||
|
|
||||||
case model.JMTaskStatusNotFound:
|
case types.JMTaskStatusNotFound:
|
||||||
// 任务未找到
|
// 任务未找到
|
||||||
s.handleTaskError(job.Id, "task not found")
|
s.handleTaskError(job.Id, "task not found")
|
||||||
|
|
||||||
case model.JMTaskStatusExpired:
|
case types.JMTaskStatusExpired:
|
||||||
continue
|
continue
|
||||||
default:
|
default:
|
||||||
logger.Warnf("unknown task status: %s", resp.Data.Status)
|
logger.Warnf("unknown task status: %s", resp.Data.Status)
|
||||||
@@ -441,6 +368,21 @@ func (s *Service) pollTaskStatus() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 找出失败的任务,并恢复其扣减算力
|
||||||
|
s.db.Where("status = ?", types.JMTaskStatusFailed).Where("power > ?", 0).Find(&jobs)
|
||||||
|
for _, job := range jobs {
|
||||||
|
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||||
|
Type: types.PowerRefund,
|
||||||
|
Model: job.ReqKey,
|
||||||
|
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d", job.Id),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 更新任务状态
|
||||||
|
s.db.Model(&job).UpdateColumn("power", 0)
|
||||||
|
}
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -448,7 +390,7 @@ func (s *Service) pollTaskStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateJobStatus 更新任务状态
|
// UpdateJobStatus 更新任务状态
|
||||||
func (s *Service) UpdateJobStatus(jobId uint, status model.JMTaskStatus, errMsg string) error {
|
func (s *Service) UpdateJobStatus(jobId uint, status types.JMTaskStatus, errMsg string) error {
|
||||||
updates := map[string]any{
|
updates := map[string]any{
|
||||||
"status": status,
|
"status": status,
|
||||||
"updated_at": time.Now(),
|
"updated_at": time.Now(),
|
||||||
@@ -462,7 +404,7 @@ func (s *Service) UpdateJobStatus(jobId uint, status model.JMTaskStatus, errMsg
|
|||||||
// handleTaskError 处理任务错误
|
// handleTaskError 处理任务错误
|
||||||
func (s *Service) handleTaskError(jobId uint, errMsg string) error {
|
func (s *Service) handleTaskError(jobId uint, errMsg string) error {
|
||||||
logger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
logger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
||||||
return s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, errMsg)
|
return s.UpdateJobStatus(jobId, types.JMTaskStatusFailed, errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushTaskToQueue 推送任务到队列(用于手动重试)
|
// PushTaskToQueue 推送任务到队列(用于手动重试)
|
||||||
@@ -473,8 +415,8 @@ func (s *Service) PushTaskToQueue(jobId uint) error {
|
|||||||
// GetTaskStats 获取任务统计信息
|
// GetTaskStats 获取任务统计信息
|
||||||
func (s *Service) GetTaskStats() (map[string]any, error) {
|
func (s *Service) GetTaskStats() (map[string]any, error) {
|
||||||
type StatResult struct {
|
type StatResult struct {
|
||||||
Status string `json:"status"`
|
Status types.JMTaskStatus `json:"status"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var stats []StatResult
|
var stats []StatResult
|
||||||
@@ -496,7 +438,7 @@ func (s *Service) GetTaskStats() (map[string]any, error) {
|
|||||||
|
|
||||||
for _, stat := range stats {
|
for _, stat := range stats {
|
||||||
result["total"] = result["total"].(int64) + stat.Count
|
result["total"] = result["total"].(int64) + stat.Count
|
||||||
result[stat.Status] = stat.Count
|
result[string(stat.Status)] = stat.Count
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -1,43 +1,9 @@
|
|||||||
package jimeng
|
package jimeng
|
||||||
|
|
||||||
import "geekai/store/model"
|
import (
|
||||||
|
"geekai/core/types"
|
||||||
// ReqKey 常量定义
|
|
||||||
const (
|
|
||||||
ReqKeyTextToImage = "high_aes_general_v30l_zt2i" // 文生图
|
|
||||||
ReqKeyImageToImagePortrait = "i2i_portrait_photo" // 图生图人像写真
|
|
||||||
ReqKeyImageEdit = "seededit_v3.0" // 图像编辑
|
|
||||||
ReqKeyImageEffects = "i2i_multi_style_zx2x" // 图像特效
|
|
||||||
ReqKeyTextToVideo = "jimeng_vgfm_t2v_l20" // 文生视频
|
|
||||||
ReqKeyImageToVideo = "jimeng_vgfm_i2v_l20" // 图生视频
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SubmitTaskRequest 提交任务请求
|
|
||||||
type SubmitTaskRequest struct {
|
|
||||||
ReqKey string `json:"req_key"`
|
|
||||||
// 文生图参数
|
|
||||||
Prompt string `json:"prompt,omitempty"`
|
|
||||||
Seed int64 `json:"seed,omitempty"`
|
|
||||||
Scale float64 `json:"scale,omitempty"`
|
|
||||||
Width int `json:"width,omitempty"`
|
|
||||||
Height int `json:"height,omitempty"`
|
|
||||||
UsePreLLM bool `json:"use_pre_llm,omitempty"`
|
|
||||||
// 图生图参数
|
|
||||||
ImageInput string `json:"image_input,omitempty"`
|
|
||||||
ImageUrls []string `json:"image_urls,omitempty"`
|
|
||||||
BinaryDataBase64 []string `json:"binary_data_base64,omitempty"`
|
|
||||||
Gpen float64 `json:"gpen,omitempty"`
|
|
||||||
Skin float64 `json:"skin,omitempty"`
|
|
||||||
SkinUnifi float64 `json:"skin_unifi,omitempty"`
|
|
||||||
GenMode string `json:"gen_mode,omitempty"`
|
|
||||||
// 图像编辑参数
|
|
||||||
// 图像特效参数
|
|
||||||
ImageInput1 string `json:"image_input1,omitempty"`
|
|
||||||
TemplateId string `json:"template_id,omitempty"`
|
|
||||||
// 视频生成参数
|
|
||||||
AspectRatio string `json:"aspect_ratio,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubmitTaskResponse 提交任务响应
|
// SubmitTaskResponse 提交任务响应
|
||||||
type SubmitTaskResponse struct {
|
type SubmitTaskResponse struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
@@ -73,7 +39,7 @@ type QueryTaskResponse struct {
|
|||||||
ImageUrls []string `json:"image_urls"`
|
ImageUrls []string `json:"image_urls"`
|
||||||
VideoUrl string `json:"video_url"`
|
VideoUrl string `json:"video_url"`
|
||||||
RespData string `json:"resp_data"`
|
RespData string `json:"resp_data"`
|
||||||
Status model.JMTaskStatus `json:"status"`
|
Status types.JMTaskStatus `json:"status"`
|
||||||
LlmResult string `json:"llm_result"`
|
LlmResult string `json:"llm_result"`
|
||||||
PeResult string `json:"pe_result"`
|
PeResult string `json:"pe_result"`
|
||||||
PredictTagsResult string `json:"predict_tags_result"`
|
PredictTagsResult string `json:"predict_tags_result"`
|
||||||
@@ -83,9 +49,73 @@ type QueryTaskResponse struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CodeSuccess = 10000
|
||||||
|
|
||||||
|
// 即梦AI错误代码常量
|
||||||
|
const (
|
||||||
|
// 成功
|
||||||
|
ECSuccess = 10000
|
||||||
|
|
||||||
|
// 请求参数错误 (50200-50215)
|
||||||
|
ECReqInvalidArgs = 50200 // 参数错误
|
||||||
|
ECReqMissingArgs = 50201 // 缺少参数
|
||||||
|
ECParseArgs = 50204 // 参数类型错误/参数缺失
|
||||||
|
ECImageSizeLimited = 50205 // 图像尺寸超过限制
|
||||||
|
ECImageEmpty = 50206 // 请求参数中没有获取到图像
|
||||||
|
ECImageDecodeError = 50207 // 图像解码错误
|
||||||
|
ECVideoEmpty = 50209 // 请求参数中没有获取到视频
|
||||||
|
ECVideoDecodeError = 50210 // 视频解码错误
|
||||||
|
ECVideoSizeLimited = 50211 // 视频尺寸超过限制
|
||||||
|
ECReqBodySizeLimited = 50213 // 请求Body过大
|
||||||
|
ECVideoTimeTooLong = 50214 // 输入视频时长过大
|
||||||
|
ECRPCProcess = 50215 // 请求处理失败
|
||||||
|
|
||||||
|
// 算法服务错误 (60102-60208)
|
||||||
|
ECJPFaceDetect = 60102 // 算法服务需要输入人脸图,但未检测到
|
||||||
|
ECFSLeaderRiskError = 60208 // 输入图片中包含敏感信息,未通过审核
|
||||||
|
|
||||||
|
// 权限和系统错误 (50400-50501)
|
||||||
|
ECAuth = 50400 // 权限校验失败
|
||||||
|
ECReqMethod = 50402 // 访问的接口不存在
|
||||||
|
ECReqLimit = 50429 // 超过调用QPS限制
|
||||||
|
ECInternal = 50500 // 服务器内部错误
|
||||||
|
ECRPCInternal = 50501 // 服务器内部RPC错误
|
||||||
|
)
|
||||||
|
|
||||||
|
// 错误代码到错误信息的映射
|
||||||
|
var errorCodeMessages = map[int]string{
|
||||||
|
// 成功
|
||||||
|
ECSuccess: "请求成功",
|
||||||
|
|
||||||
|
// 请求参数错误
|
||||||
|
ECReqInvalidArgs: "参数错误,检查入参及MIME类型",
|
||||||
|
ECReqMissingArgs: "缺少参数,检查入参及MIME类型",
|
||||||
|
ECParseArgs: "参数类型错误/参数缺失,检查入参及MIME类型",
|
||||||
|
ECImageSizeLimited: "图像尺寸超过限制,参考接口文档入参要求部分",
|
||||||
|
ECImageEmpty: "请求参数中没有获取到图像,检查入参",
|
||||||
|
ECImageDecodeError: "图像解码错误:没有获取到图像或者通过image_base64参数传递图像是base64解码错误,检查输出图片或检查base64是否错误携带前缀",
|
||||||
|
ECVideoEmpty: "请求参数中没有获取到视频。输入为视频时可能返回此错误,检查入参",
|
||||||
|
ECVideoDecodeError: "视频解码错误。输入为视频时可能返回此错误,检查输入视频是否不正确",
|
||||||
|
ECVideoSizeLimited: "视频尺寸超过限制。输入为视频时可能返回此错误,检查输入视频大小",
|
||||||
|
ECReqBodySizeLimited: "请求Body过大,超出接口限制,检查请求Body大小",
|
||||||
|
ECVideoTimeTooLong: "输入视频时长过大,检查输入视频时长",
|
||||||
|
ECRPCProcess: "由于输入的图片、视频、参数等不满足要求,导致请求处理失败。若接口文档中有具体说明,优先参考其具体含义,按照具体服务说明进行检查",
|
||||||
|
|
||||||
|
// 算法服务错误
|
||||||
|
ECJPFaceDetect: "算法服务需要输入人脸图,但未检测到,检查输入图片是否包含人脸",
|
||||||
|
ECFSLeaderRiskError: "输入图片中包含敏感信息,未通过审核",
|
||||||
|
|
||||||
|
// 权限和系统错误
|
||||||
|
ECAuth: "权限校验失败,请检查是否已创建应用并开通服务或签名,参考接入指南及快速接入",
|
||||||
|
ECReqMethod: "访问的接口不存在,检查入参",
|
||||||
|
ECReqLimit: "超过调用QPS限制,购买QPS增项包",
|
||||||
|
ECInternal: "服务器内部错误,提工单",
|
||||||
|
ECRPCInternal: "服务器内部RPC错误,提工单",
|
||||||
|
}
|
||||||
|
|
||||||
// CreateTaskRequest 创建任务请求
|
// CreateTaskRequest 创建任务请求
|
||||||
type CreateTaskRequest struct {
|
type CreateTaskRequest struct {
|
||||||
Type model.JMTaskType `json:"type"`
|
Type types.JMTaskType `json:"type"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
Params map[string]any `json:"params"`
|
Params map[string]any `json:"params"`
|
||||||
ReqKey string `json:"req_key"`
|
ReqKey string `json:"req_key"`
|
||||||
@@ -93,53 +123,14 @@ type CreateTaskRequest struct {
|
|||||||
Power int `json:"power,omitempty"`
|
Power int `json:"power,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoInfo 水印信息
|
|
||||||
type LogoInfo struct {
|
|
||||||
AddLogo bool `json:"add_logo"`
|
|
||||||
Position int `json:"position"`
|
|
||||||
Language int `json:"language"`
|
|
||||||
Opacity float64 `json:"opacity"`
|
|
||||||
LogoTextContent string `json:"logo_text_content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReqJsonConfig 查询配置
|
|
||||||
type ReqJsonConfig struct {
|
|
||||||
ReturnUrl bool `json:"return_url"`
|
|
||||||
LogoInfo *LogoInfo `json:"logo_info,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageEffectTemplate 图像特效模板
|
|
||||||
const (
|
const (
|
||||||
TemplateIdFelt3DPolaroid = "felt_3d_polaroid" // 毛毡3d拍立得风格
|
ImageEffectReqKey = "i2i_multi_style_zx2x"
|
||||||
TemplateIdMyWorld = "my_world" // 像素世界风
|
DoubaoSeedream40ReqKey = "doubao-seedream-4-0-250828"
|
||||||
TemplateIdMyWorldUniversal = "my_world_universal" // 像素世界-万物通用版
|
|
||||||
TemplateIdPlasticBubbleFigure = "plastic_bubble_figure" // 盲盒玩偶风
|
|
||||||
TemplateIdPlasticBubbleFigureCartoon = "plastic_bubble_figure_cartoon_text" // 塑料泡罩人偶-文字卡头版
|
|
||||||
TemplateIdFurryDreamDoll = "furry_dream_doll" // 毛绒玩偶风
|
|
||||||
TemplateIdMicroLandscapeMiniWorld = "micro_landscape_mini_world" // 迷你世界玩偶风
|
|
||||||
TemplateIdMicroLandscapeProfessional = "micro_landscape_mini_world_professional" // 微型景观小世界-职业版
|
|
||||||
TemplateIdAcrylicOrnaments = "acrylic_ornaments" // 亚克力挂饰
|
|
||||||
TemplateIdFeltKeychain = "felt_keychain" // 毛毡钥匙扣
|
|
||||||
TemplateIdLofiPixelCharacter = "lofi_pixel_character_mini_card" // Lofi像素人物小卡
|
|
||||||
TemplateIdAngelFigurine = "angel_figurine" // 天使形象手办
|
|
||||||
TemplateIdLyingInFluffyBelly = "lying_in_fluffy_belly" // 躺在毛茸茸肚皮里
|
|
||||||
TemplateIdGlassBall = "glass_ball" // 玻璃球
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AspectRatio 视频宽高比
|
|
||||||
const (
|
const (
|
||||||
AspectRatio16_9 = "16:9" // 1280×720
|
ASyncActionSubmit = "CVSync2AsyncSubmitTask" // 异步提交任务
|
||||||
AspectRatio9_16 = "9:16" // 720×1280
|
SyncActionSubmit = "CVSubmitTask" // 同步提交任务
|
||||||
AspectRatio1_1 = "1:1" // 960×960
|
ASyncActionGetResult = "CVSync2AsyncGetResult" // 异步获取结果
|
||||||
AspectRatio4_3 = "4:3" // 960×720
|
SyncActionGetResult = "CVGetResult" // 同步获取结果
|
||||||
AspectRatio3_4 = "3:4" // 720×960
|
|
||||||
AspectRatio21_9 = "21:9" // 1680×720
|
|
||||||
AspectRatio9_21 = "9:21" // 720×1680
|
|
||||||
)
|
|
||||||
|
|
||||||
// GenMode 生成模式
|
|
||||||
const (
|
|
||||||
GenModeCreative = "creative" // 提示词模式
|
|
||||||
GenModeReference = "reference" // 全参考模式
|
|
||||||
GenModeReferenceChar = "reference_char" // 人物参考模式
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
// * 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/types"
|
|
||||||
"geekai/store/model"
|
|
||||||
"geekai/utils"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"github.com/shirou/gopsutil/host"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LicenseService struct {
|
|
||||||
license *types.License
|
|
||||||
urlWhiteList []string
|
|
||||||
machineId string
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLicenseService(sysConfig *types.SystemConfig, db *gorm.DB) *LicenseService {
|
|
||||||
var machineId string
|
|
||||||
info, err := host.Info()
|
|
||||||
if err == nil {
|
|
||||||
machineId = info.HostID
|
|
||||||
}
|
|
||||||
logger.Infof("License: %+v", sysConfig.License)
|
|
||||||
return &LicenseService{
|
|
||||||
license: &sysConfig.License,
|
|
||||||
machineId: machineId,
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type License struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
License string `json:"license"`
|
|
||||||
MachineId string `json:"mid"`
|
|
||||||
ActiveAt int64 `json:"active_at"`
|
|
||||||
ExpiredAt int64 `json:"expired_at"`
|
|
||||||
UserNum int `json:"user_num"`
|
|
||||||
Configs types.LicenseConfig `json:"configs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveLicense 激活 License
|
|
||||||
func (s *LicenseService) ActiveLicense(license string) error {
|
|
||||||
var res struct {
|
|
||||||
Code types.BizCode `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data License `json:"data"`
|
|
||||||
}
|
|
||||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/active")
|
|
||||||
response, err := req.C().R().
|
|
||||||
SetBody(map[string]string{"license": license, "machine_id": s.machineId}).
|
|
||||||
SetSuccessResult(&res).Post(apiURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("发送激活请求失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.IsErrorState() {
|
|
||||||
return fmt.Errorf("发送激活请求失败:%v", response.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return fmt.Errorf("激活失败:%v", res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Data.ExpiredAt > 0 && res.Data.ExpiredAt < time.Now().Unix() {
|
|
||||||
return fmt.Errorf("License 已过期")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.license = &types.License{
|
|
||||||
Key: license,
|
|
||||||
MachineId: s.machineId,
|
|
||||||
Configs: res.Data.Configs,
|
|
||||||
ExpiredAt: res.Data.ExpiredAt,
|
|
||||||
IsActive: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存 License 到数据库
|
|
||||||
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("保存 License 到数据库失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncLicense 定期同步 License
|
|
||||||
func (s *LicenseService) SyncLicense() {
|
|
||||||
go func() {
|
|
||||||
retryCounter := 0
|
|
||||||
for {
|
|
||||||
license, err := s.fetchLicense()
|
|
||||||
if err != nil {
|
|
||||||
retryCounter++
|
|
||||||
if retryCounter < 5 {
|
|
||||||
logger.Debug(err)
|
|
||||||
}
|
|
||||||
s.license.IsActive = false
|
|
||||||
} else {
|
|
||||||
s.license = license
|
|
||||||
// 保存 License 到数据库
|
|
||||||
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("保存 License 到数据库失败: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
urls, err := s.fetchUrlWhiteList()
|
|
||||||
if err == nil {
|
|
||||||
s.urlWhiteList = urls
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LicenseService) fetchLicense() (*types.License, error) {
|
|
||||||
var res struct {
|
|
||||||
Code types.BizCode `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data License `json:"data"`
|
|
||||||
}
|
|
||||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/check")
|
|
||||||
response, err := req.C().R().
|
|
||||||
SetBody(map[string]string{"license": s.license.Key, "machine_id": s.machineId}).
|
|
||||||
SetSuccessResult(&res).Post(apiURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("License 同步失败: %v", err)
|
|
||||||
}
|
|
||||||
if response.IsErrorState() {
|
|
||||||
return nil, fmt.Errorf("License 同步失败:%v", response.Status)
|
|
||||||
}
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return nil, fmt.Errorf("License 同步失败:%v", res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.License{
|
|
||||||
Key: res.Data.License,
|
|
||||||
MachineId: res.Data.MachineId,
|
|
||||||
Configs: res.Data.Configs,
|
|
||||||
ExpiredAt: res.Data.ExpiredAt,
|
|
||||||
IsActive: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
|
|
||||||
var res struct {
|
|
||||||
Code types.BizCode `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data []string `json:"data"`
|
|
||||||
}
|
|
||||||
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/urls")
|
|
||||||
response, err := req.C().R().SetSuccessResult(&res).Get(apiURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
|
||||||
}
|
|
||||||
if response.IsErrorState() {
|
|
||||||
return nil, fmt.Errorf("发送请求失败:%v", response.Status)
|
|
||||||
}
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return nil, fmt.Errorf("获取白名单失败:%v", res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLicense 获取许可信息
|
|
||||||
func (s *LicenseService) GetLicense() *types.License {
|
|
||||||
if s.license == nil {
|
|
||||||
var config model.Config
|
|
||||||
s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).First(&config)
|
|
||||||
if config.Value != "" {
|
|
||||||
utils.JsonDecode(config.Value, &s.license)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s.license
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LicenseService) SetLicense(licenseKey string) {
|
|
||||||
s.license.Key = licenseKey
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValidApiURL 判断是否合法的中转 URL
|
|
||||||
func (s *LicenseService) IsValidApiURL(uri string) error {
|
|
||||||
// 获得许可授权的直接放行
|
|
||||||
if s.license.IsActive {
|
|
||||||
if s.license.MachineId != s.machineId {
|
|
||||||
return errors.New("系统使用了盗版的许可证书")
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().Unix() > s.license.ExpiredAt {
|
|
||||||
return errors.New("系统许可证书已经过期")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.urlWhiteList) == 0 {
|
|
||||||
urls, err := s.fetchUrlWhiteList()
|
|
||||||
if err == nil {
|
|
||||||
s.urlWhiteList = urls
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range s.urlWhiteList {
|
|
||||||
if strings.HasPrefix(uri, v) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("当前 API 地址 %s 不在白名单列表当中。", uri)
|
|
||||||
}
|
|
||||||
@@ -8,16 +8,17 @@ package service
|
|||||||
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/store"
|
"geekai/store"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -29,61 +30,29 @@ const (
|
|||||||
|
|
||||||
// MigrationService 配置迁移服务
|
// MigrationService 配置迁移服务
|
||||||
type MigrationService struct {
|
type MigrationService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
redisClient *redis.Client
|
redisClient *redis.Client
|
||||||
appConfig *types.AppConfig
|
appConfig *types.AppConfig
|
||||||
levelDB *store.LevelDB
|
levelDB *store.LevelDB
|
||||||
licenseService *LicenseService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig, levelDB *store.LevelDB, licenseService *LicenseService) *MigrationService {
|
func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig, levelDB *store.LevelDB) *MigrationService {
|
||||||
return &MigrationService{
|
return &MigrationService{
|
||||||
db: db,
|
db: db,
|
||||||
redisClient: redisClient,
|
redisClient: redisClient,
|
||||||
appConfig: appConfig,
|
appConfig: appConfig,
|
||||||
levelDB: levelDB,
|
levelDB: levelDB,
|
||||||
licenseService: licenseService,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MigrationService) StartMigrate() {
|
func (s *MigrationService) StartMigrate() {
|
||||||
|
// 表结构同步必须在对外服务前完成,避免缺列导致业务报错
|
||||||
|
s.TableMigration()
|
||||||
go func() {
|
go func() {
|
||||||
s.MigrateConfig(s.appConfig)
|
_ = s.MigrateConfig(s.appConfig)
|
||||||
s.TableMigration()
|
|
||||||
s.MigrateLicense()
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 迁移 License
|
|
||||||
func (s *MigrationService) MigrateLicense() {
|
|
||||||
key := "migrate:license"
|
|
||||||
if s.redisClient.Get(context.Background(), key).Val() == "1" {
|
|
||||||
logger.Info("License 已迁移,跳过迁移")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("开始迁移 License...")
|
|
||||||
var license types.License
|
|
||||||
err := s.levelDB.Get(types.LicenseKey, &license)
|
|
||||||
if err != nil {
|
|
||||||
license = types.License{
|
|
||||||
Key: "",
|
|
||||||
MachineId: "",
|
|
||||||
Configs: types.LicenseConfig{UserNum: 0, DeCopy: false},
|
|
||||||
ExpiredAt: 0,
|
|
||||||
IsActive: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.Infof("迁移 License: %+v", license)
|
|
||||||
if err := s.saveConfig(types.ConfigKeyLicense, license); err != nil {
|
|
||||||
logger.Errorf("迁移 License 失败: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.licenseService.SetLicense(license.Key)
|
|
||||||
logger.Info("迁移 License 完成")
|
|
||||||
s.redisClient.Set(context.Background(), key, "1", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 迁移配置内容
|
// 迁移配置内容
|
||||||
func (s *MigrationService) MigrateConfigContent() error {
|
func (s *MigrationService) MigrateConfigContent() error {
|
||||||
// 用户协议
|
// 用户协议
|
||||||
@@ -157,66 +126,163 @@ func (s *MigrationService) MigrateConfigContent() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据表迁移
|
// 永不删除的保护列(大小写不敏感)
|
||||||
func (s *MigrationService) TableMigration() {
|
var protectedColumns = map[string]struct{}{
|
||||||
// 新数据表
|
"id": {},
|
||||||
s.db.AutoMigrate(&model.Moderation{})
|
"created_at": {},
|
||||||
|
"updated_at": {},
|
||||||
|
}
|
||||||
|
|
||||||
// 订单字段整理
|
// allModels 全部需要同步的数据表 model
|
||||||
if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") {
|
func allModels() []any {
|
||||||
s.db.Migrator().RenameColumn(&model.Order{}, "pay_type", "channel")
|
return []any{
|
||||||
}
|
&model.AdminUser{},
|
||||||
if !s.db.Migrator().HasColumn(&model.Order{}, "checked") {
|
&model.ApiKey{},
|
||||||
s.db.Migrator().AddColumn(&model.Order{}, "checked")
|
&model.AppType{},
|
||||||
}
|
&model.ChatApp{},
|
||||||
|
&model.ChatItem{},
|
||||||
// 重命名 config 表字段
|
&model.ChatMessage{},
|
||||||
if s.db.Migrator().HasColumn(&model.Config{}, "config_json") {
|
&model.ChatModel{},
|
||||||
s.db.Migrator().RenameColumn(&model.Config{}, "config_json", "value")
|
&model.Config{},
|
||||||
}
|
&model.DallJob{},
|
||||||
if s.db.Migrator().HasColumn(&model.Config{}, "marker") {
|
&model.File{},
|
||||||
s.db.Migrator().RenameColumn(&model.Config{}, "marker", "name")
|
&model.Function{},
|
||||||
}
|
&model.InviteCode{},
|
||||||
if s.db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
|
&model.InviteLog{},
|
||||||
s.db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
|
&model.JimengJob{},
|
||||||
}
|
&model.Menu{},
|
||||||
if s.db.Migrator().HasIndex(&model.Config{}, "marker") {
|
&model.MidJourneyJob{},
|
||||||
s.db.Migrator().DropIndex(&model.Config{}, "marker")
|
&model.Moderation{},
|
||||||
}
|
&model.Order{},
|
||||||
|
&model.PowerLog{},
|
||||||
// 手动删除字段
|
&model.Product{},
|
||||||
if s.db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
|
&model.Redeem{},
|
||||||
s.db.Migrator().DropColumn(&model.Order{}, "deleted_at")
|
&model.SdJob{},
|
||||||
}
|
&model.SunoJob{},
|
||||||
if s.db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") {
|
&model.User{},
|
||||||
s.db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at")
|
&model.UserLoginLog{},
|
||||||
}
|
&model.VideoJob{},
|
||||||
if s.db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") {
|
|
||||||
s.db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at")
|
|
||||||
}
|
|
||||||
if s.db.Migrator().HasColumn(&model.User{}, "chat_config") {
|
|
||||||
s.db.Migrator().DropColumn(&model.User{}, "chat_config")
|
|
||||||
}
|
|
||||||
if s.db.Migrator().HasColumn(&model.ChatModel{}, "category") {
|
|
||||||
s.db.Migrator().DropColumn(&model.ChatModel{}, "category")
|
|
||||||
}
|
|
||||||
if s.db.Migrator().HasColumn(&model.ChatModel{}, "description") {
|
|
||||||
s.db.Migrator().DropColumn(&model.ChatModel{}, "description")
|
|
||||||
}
|
|
||||||
if s.db.Migrator().HasColumn(&model.Product{}, "discount") {
|
|
||||||
s.db.Migrator().DropColumn(&model.Product{}, "discount")
|
|
||||||
}
|
|
||||||
if s.db.Migrator().HasColumn(&model.Product{}, "days") {
|
|
||||||
s.db.Migrator().DropColumn(&model.Product{}, "days")
|
|
||||||
}
|
|
||||||
if s.db.Migrator().HasColumn(&model.Product{}, "app_url") {
|
|
||||||
s.db.Migrator().DropColumn(&model.Product{}, "app_url")
|
|
||||||
}
|
|
||||||
if s.db.Migrator().HasColumn(&model.Product{}, "url") {
|
|
||||||
s.db.Migrator().DropColumn(&model.Product{}, "url")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据表迁移:先处理字段重命名(保数据),再全量同步 schema
|
||||||
|
func (s *MigrationService) TableMigration() {
|
||||||
|
logger.Info("开始数据表迁移...")
|
||||||
|
s.renameColumns()
|
||||||
|
if err := s.SyncAllModels(); err != nil {
|
||||||
|
logger.Errorf("同步数据表字段失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("数据表迁移完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
// renameColumns 只处理「改名」场景:删旧加新会丢数据,必须先 Rename
|
||||||
|
func (s *MigrationService) renameColumns() {
|
||||||
|
m := s.db.Migrator()
|
||||||
|
|
||||||
|
if m.HasColumn(&model.JimengJob{}, "task_params") {
|
||||||
|
_ = m.RenameColumn(&model.JimengJob{}, "task_params", "params")
|
||||||
|
}
|
||||||
|
if m.HasColumn(&model.Order{}, "pay_type") {
|
||||||
|
_ = m.RenameColumn(&model.Order{}, "pay_type", "channel")
|
||||||
|
}
|
||||||
|
if m.HasColumn(&model.Config{}, "config_json") {
|
||||||
|
_ = m.RenameColumn(&model.Config{}, "config_json", "value")
|
||||||
|
}
|
||||||
|
if m.HasColumn(&model.Config{}, "marker") {
|
||||||
|
_ = m.RenameColumn(&model.Config{}, "marker", "name")
|
||||||
|
}
|
||||||
|
if m.HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
|
||||||
|
_ = m.DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
|
||||||
|
}
|
||||||
|
if m.HasIndex(&model.Config{}, "marker") {
|
||||||
|
_ = m.DropIndex(&model.Config{}, "marker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncAllModels 按 model 定义同步所有数据表:缺列新建,多余列删除
|
||||||
|
func (s *MigrationService) SyncAllModels() error {
|
||||||
|
var firstErr error
|
||||||
|
for _, m := range allModels() {
|
||||||
|
if err := s.syncModel(m); err != nil {
|
||||||
|
logger.Errorf("同步 model %T 失败: %v", m, err)
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrationService) syncModel(dst any) error {
|
||||||
|
tableName := s.tableName(dst)
|
||||||
|
if err := s.db.AutoMigrate(dst); err != nil {
|
||||||
|
return fmt.Errorf("AutoMigrate %s: %w", tableName, err)
|
||||||
|
}
|
||||||
|
if err := s.dropUnusedColumns(dst); err != nil {
|
||||||
|
return fmt.Errorf("drop unused columns %s: %w", tableName, err)
|
||||||
|
}
|
||||||
|
logger.Infof("已同步数据表: %s", tableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrationService) dropUnusedColumns(dst any) error {
|
||||||
|
dbCols, err := s.db.Migrator().ColumnTypes(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
modelCols, err := s.modelColumnNames(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, col := range dbCols {
|
||||||
|
name := col.Name()
|
||||||
|
if s.isProtectedColumn(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := modelCols[strings.ToLower(name)]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Infof("删除多余字段: %s.%s", s.tableName(dst), name)
|
||||||
|
if err := s.db.Migrator().DropColumn(dst, name); err != nil {
|
||||||
|
return fmt.Errorf("DropColumn %s: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrationService) modelColumnNames(dst any) (map[string]struct{}, error) {
|
||||||
|
parsed, err := schema.Parse(dst, &schemaCache, s.db.Config.NamingStrategy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cols := make(map[string]struct{}, len(parsed.Fields))
|
||||||
|
for _, field := range parsed.Fields {
|
||||||
|
if field.DBName == "" || field.IgnoreMigration {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cols[strings.ToLower(field.DBName)] = struct{}{}
|
||||||
|
}
|
||||||
|
return cols, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrationService) isProtectedColumn(name string) bool {
|
||||||
|
_, ok := protectedColumns[strings.ToLower(name)]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrationService) tableName(dst any) string {
|
||||||
|
stmt := &gorm.Statement{DB: s.db}
|
||||||
|
if err := stmt.Parse(dst); err != nil {
|
||||||
|
return fmt.Sprintf("%T", dst)
|
||||||
|
}
|
||||||
|
return stmt.Schema.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
// schema.Parse 进程内复用的 schema cache
|
||||||
|
var schemaCache sync.Map
|
||||||
|
|
||||||
// 迁移配置数据
|
// 迁移配置数据
|
||||||
func (s *MigrationService) MigrateConfig(config *types.AppConfig) error {
|
func (s *MigrationService) MigrateConfig(config *types.AppConfig) error {
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
logger2 "geekai/logger"
|
logger2 "geekai/logger"
|
||||||
"geekai/service"
|
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/utils"
|
"geekai/utils"
|
||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
@@ -26,9 +25,8 @@ import (
|
|||||||
|
|
||||||
// Client MidJourney client
|
// Client MidJourney client
|
||||||
type Client struct {
|
type Client struct {
|
||||||
client *req.Client
|
client *req.Client
|
||||||
licenseService *service.LicenseService
|
db *gorm.DB
|
||||||
db *gorm.DB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageReq struct {
|
type ImageReq struct {
|
||||||
@@ -77,11 +75,10 @@ type QueryRes struct {
|
|||||||
|
|
||||||
var logger = logger2.GetLogger()
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
func NewClient(licenseService *service.LicenseService, db *gorm.DB) *Client {
|
func NewClient(db *gorm.DB) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||||
licenseService: licenseService,
|
db: db,
|
||||||
db: db,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,10 +195,6 @@ func (c *Client) doRequest(body interface{}, apiPath string, channel string) (Im
|
|||||||
return ImageRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
|
return ImageRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.licenseService.IsValidApiURL(apiKey.ApiURL); err != nil {
|
|
||||||
return ImageRes{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("%s/%s", apiKey.ApiURL, apiPath)
|
apiURL := fmt.Sprintf("%s/%s", apiKey.ApiURL, apiPath)
|
||||||
logger.Info("API URL: ", apiURL)
|
logger.Info("API URL: ", apiURL)
|
||||||
r, err := req.C().R().
|
r, err := req.C().R().
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package sora
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"geekai/service/oss"
|
||||||
|
"geekai/store/vo"
|
||||||
|
"geekai/utils"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
logger2 "geekai/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
type SoraService struct {
|
||||||
|
uploadManager *oss.UploaderManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSoraService(uploadManager *oss.UploaderManager) *SoraService {
|
||||||
|
return &SoraService{
|
||||||
|
uploadManager: uploadManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载视频地址
|
||||||
|
func (s *SoraService) DownloadVideoURL(text string) (*vo.File, error) {
|
||||||
|
videoDataURL, err := s.ExtractVideoURL(text)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 JSON 数据
|
||||||
|
resp, err := http.Get(videoDataURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析视频下载地址
|
||||||
|
var videoData map[string]any
|
||||||
|
err = json.Unmarshal(body, &videoData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := videoData["url"].(string); ok && v != "" {
|
||||||
|
logger.Infof("try to download video: %s", v)
|
||||||
|
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v, ".mp4", true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件大小
|
||||||
|
size, _ := utils.GetFileSize(videoURL)
|
||||||
|
name := filepath.Base(videoURL)
|
||||||
|
return &vo.File{
|
||||||
|
Name: name,
|
||||||
|
ObjKey: name,
|
||||||
|
URL: videoURL,
|
||||||
|
Ext: ".mp4",
|
||||||
|
Size: size,
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("no video URL found: " + string(body))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从文本中提取视频URL
|
||||||
|
func (s *SoraService) ExtractVideoURL(text string) (string, error) {
|
||||||
|
// 提取原始 JSON 数据地址
|
||||||
|
//[原始数据](https://asyncdata.net/source/task_01k8pye324ef7t6heq6jyaxbfe) 类似这样的链接
|
||||||
|
re := regexp.MustCompile(`\[原始数据\]\((https?://.*?)\)`)
|
||||||
|
matches := re.FindAllStringSubmatch(text, 1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return "", errors.New("no video URL found")
|
||||||
|
}
|
||||||
|
return matches[0][1], nil
|
||||||
|
}
|
||||||
@@ -57,12 +57,19 @@ func (s *UserService) DecreasePower(userId uint, power int, log model.PowerLog)
|
|||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
tx := s.db.Begin()
|
tx := s.db.Begin()
|
||||||
|
var user model.User
|
||||||
|
tx.Where("id", userId).First(&user)
|
||||||
|
if user.Power < power {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("用户算力不足")
|
||||||
|
}
|
||||||
|
|
||||||
err := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", power)).Error
|
err := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", power)).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return fmt.Errorf("扣减算力失败:%v", err)
|
return fmt.Errorf("扣减算力失败:%v", err)
|
||||||
}
|
}
|
||||||
var user model.User
|
|
||||||
tx.Where("id", userId).First(&user)
|
tx.Where("id", userId).First(&user)
|
||||||
err = tx.Create(&model.PowerLog{
|
err = tx.Create(&model.PowerLog{
|
||||||
UserId: user.Id,
|
UserId: user.Id,
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ const (
|
|||||||
LoginStatusExpired = "expired" // 登录失效,需要重新登录
|
LoginStatusExpired = "expired" // 登录失效,需要重新登录
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewWxLoginService(config types.WxLoginConfig, redisClient *redis.Client) *WxLoginService {
|
func NewWxLoginService(sysConfig *types.SystemConfig, redisClient *redis.Client) *WxLoginService {
|
||||||
return &WxLoginService{
|
return &WxLoginService{
|
||||||
config: config,
|
config: sysConfig.WxLogin,
|
||||||
client: req.C().SetTimeout(10 * time.Second),
|
client: req.C().SetTimeout(10 * time.Second),
|
||||||
redisClient: redisClient,
|
redisClient: redisClient,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type AdminUser struct {
|
|||||||
Salt string `gorm:"column:salt;type:char(12);not null;comment:密码盐" json:"salt"`
|
Salt string `gorm:"column:salt;type:char(12);not null;comment:密码盐" json:"salt"`
|
||||||
Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"`
|
Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"`
|
||||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
LastLoginIp string `gorm:"column:last_login_ip;type:char(32);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type Function struct {
|
|||||||
Token string `gorm:"column:token;type:varchar(255);comment:API授权token" json:"token"`
|
Token string `gorm:"column:token;type:varchar(255);comment:API授权token" json:"token"`
|
||||||
Action string `gorm:"column:action;type:varchar(255);comment:函数处理 API" json:"action"`
|
Action string `gorm:"column:action;type:varchar(255);comment:函数处理 API" json:"action"`
|
||||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启用" json:"enabled"`
|
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启用" json:"enabled"`
|
||||||
|
Power int `gorm:"column:power;type:int(11);not null;default:0;comment:消费算力点数" json:"power"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Function) TableName() string {
|
func (m *Function) TableName() string {
|
||||||
|
|||||||
@@ -1,54 +1,30 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"geekai/core/types"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JimengJob 即梦AI任务模型
|
// JimengJob 即梦AI任务模型
|
||||||
type JimengJob struct {
|
type JimengJob struct {
|
||||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
UserId uint `gorm:"column:user_id;type:int(11);not null;index;comment:用户ID" json:"user_id"`
|
UserId uint `gorm:"column:user_id;type:int(11);not null;index;comment:用户ID" json:"user_id"`
|
||||||
TaskId string `gorm:"column:task_id;type:varchar(100);not null;index;comment:任务ID" json:"task_id"`
|
TaskId string `gorm:"column:task_id;type:varchar(100);not null;index;comment:任务ID" json:"task_id"`
|
||||||
Type JMTaskType `gorm:"column:type;type:varchar(50);not null;comment:任务类型" json:"type"`
|
Type types.JMTaskType `gorm:"column:type;type:varchar(50);not null;comment:任务类型" json:"type"`
|
||||||
ReqKey string `gorm:"column:req_key;type:varchar(100);comment:请求Key" json:"req_key"`
|
ReqKey string `gorm:"column:req_key;type:varchar(100);comment:请求Key" json:"req_key"`
|
||||||
Prompt string `gorm:"column:prompt;type:text;comment:提示词" json:"prompt"`
|
Prompt string `gorm:"column:prompt;type:text;comment:提示词" json:"prompt"`
|
||||||
TaskParams string `gorm:"column:task_params;type:text;comment:任务参数JSON" json:"task_params"`
|
Params string `gorm:"column:params;type:text;comment:任务参数JSON" json:"params"`
|
||||||
ImgURL string `gorm:"column:img_url;type:varchar(1024);comment:图片或封面URL" json:"img_url"`
|
ImgURL string `gorm:"column:img_url;type:varchar(1024);comment:图片或封面URL" json:"img_url"`
|
||||||
VideoURL string `gorm:"column:video_url;type:varchar(1024);comment:视频URL" json:"video_url"`
|
VideoURL string `gorm:"column:video_url;type:varchar(1024);comment:视频URL" json:"video_url"`
|
||||||
RawData string `gorm:"column:raw_data;type:text;comment:原始API响应" json:"raw_data"`
|
RawData string `gorm:"column:raw_data;type:text;comment:原始API响应" json:"raw_data"`
|
||||||
Progress int `gorm:"column:progress;type:int;default:0;comment:进度百分比" json:"progress"`
|
Progress int `gorm:"column:progress;type:int;default:0;comment:进度百分比" json:"progress"`
|
||||||
Status JMTaskStatus `gorm:"column:status;type:varchar(20);default:'pending';comment:任务状态" json:"status"`
|
Status types.JMTaskStatus `gorm:"column:status;type:varchar(20);default:'pending';comment:任务状态" json:"status"`
|
||||||
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
||||||
Power int `gorm:"column:power;type:int(11);default:0;comment:消耗算力" json:"power"`
|
Power int `gorm:"column:power;type:int(11);default:0;comment:消耗算力" json:"power"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JMTaskStatus 任务状态
|
|
||||||
type JMTaskStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
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 (
|
|
||||||
JMTaskTypeTextToImage = JMTaskType("text_to_image") // 文生图
|
|
||||||
JMTaskTypeImageToImage = JMTaskType("image_to_image") // 图生图
|
|
||||||
JMTaskTypeImageEdit = JMTaskType("image_edit") // 图像编辑
|
|
||||||
JMTaskTypeImageEffects = JMTaskType("image_effects") // 图像特效
|
|
||||||
JMTaskTypeTextToVideo = JMTaskType("text_to_video") // 文生视频
|
|
||||||
JMTaskTypeImageToVideo = JMTaskType("image_to_video") // 图生视频
|
|
||||||
)
|
|
||||||
|
|
||||||
// TableName 返回数据表名称
|
// TableName 返回数据表名称
|
||||||
func (JimengJob) TableName() string {
|
func (JimengJob) TableName() string {
|
||||||
return "geekai_jimeng_jobs"
|
return "geekai_jimeng_jobs"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type User struct {
|
|||||||
ChatModels string `gorm:"column:chat_models_json;type:text;default:null;comment:AI模型 json" json:"chat_models"`
|
ChatModels string `gorm:"column:chat_models_json;type:text;default:null;comment:AI模型 json" json:"chat_models"`
|
||||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||||
Vip bool `gorm:"column:vip;type:tinyint(1);not null;default:0;comment:是否会员" json:"vip"`
|
Vip bool `gorm:"column:vip;type:tinyint(1);not null;default:0;comment:是否会员" json:"vip"`
|
||||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
LastLoginIp string `gorm:"column:last_login_ip;type:char(32);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||||
OpenId string `gorm:"column:openid;type:varchar(100);comment:第三方登录账号ID" json:"openid"`
|
OpenId string `gorm:"column:openid;type:varchar(100);comment:第三方登录账号ID" json:"openid"`
|
||||||
Platform string `gorm:"column:platform;type:varchar(30);comment:登录平台" json:"platform"`
|
Platform string `gorm:"column:platform;type:varchar(30);comment:登录平台" json:"platform"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type UserLoginLog struct {
|
|||||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户ID" json:"user_id"`
|
UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户ID" json:"user_id"`
|
||||||
Username string `gorm:"column:username;type:varchar(30);not null;comment:用户名" json:"username"`
|
Username string `gorm:"column:username;type:varchar(30);not null;comment:用户名" json:"username"`
|
||||||
LoginIp string `gorm:"column:login_ip;type:char(16);not null;comment:登录IP" json:"login_ip"`
|
LoginIp string `gorm:"column:login_ip;type:char(32);not null;comment:登录IP" json:"login_ip"`
|
||||||
LoginAddress string `gorm:"column:login_address;type:varchar(30);not null;comment:登录地址" json:"login_address"`
|
LoginAddress string `gorm:"column:login_address;type:varchar(30);not null;comment:登录地址" json:"login_address"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ type Function struct {
|
|||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
Power int `json:"power"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
package vo
|
package vo
|
||||||
|
|
||||||
import "geekai/store/model"
|
import "geekai/core/types"
|
||||||
|
|
||||||
// JimengJob 即梦AI任务VO
|
// JimengJob 即梦AI任务VO
|
||||||
type JimengJob struct {
|
type JimengJob struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
UserId uint `json:"user_id"`
|
UserId uint `json:"user_id"`
|
||||||
TaskId string `json:"task_id"`
|
TaskId string `json:"task_id"`
|
||||||
Type model.JMTaskType `json:"type"`
|
Type types.JMTaskType `json:"type"`
|
||||||
ReqKey string `json:"req_key"`
|
ReqKey string `json:"req_key"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
TaskParams string `json:"task_params"`
|
Params map[string]any `json:"params"`
|
||||||
ImgURL string `json:"img_url"`
|
ImgURL string `json:"img_url"`
|
||||||
VideoURL string `json:"video_url"`
|
VideoURL string `json:"video_url"`
|
||||||
RawData string `json:"raw_data"`
|
RawData string `json:"raw_data"`
|
||||||
Progress int `json:"progress"`
|
Progress int `json:"progress"`
|
||||||
Status model.JMTaskStatus `json:"status"`
|
Status types.JMTaskStatus `json:"status"`
|
||||||
ErrMsg string `json:"err_msg"`
|
ErrMsg string `json:"err_msg"`
|
||||||
Power int `json:"power"`
|
Power int `json:"power"`
|
||||||
CreatedAt int64 `json:"created_at"` // 时间戳
|
CreatedAt int64 `json:"created_at"` // 时间戳
|
||||||
UpdatedAt int64 `json:"updated_at"` // 时间戳
|
UpdatedAt int64 `json:"updated_at"` // 时间戳
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,817 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AudioDuration returns duration of an audio file.
|
||||||
|
// Supported formats: MP3, WAV (auto-detected by header)
|
||||||
|
func AudioDuration(path string) (time.Duration, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Peek first 12 bytes to detect format
|
||||||
|
head := make([]byte, 12)
|
||||||
|
n, err := io.ReadFull(f, head)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if n < 12 {
|
||||||
|
return 0, errors.New("file too small")
|
||||||
|
}
|
||||||
|
|
||||||
|
// WAV: RIFF....WAVE
|
||||||
|
if string(head[0:4]) == "RIFF" && string(head[8:12]) == "WAVE" {
|
||||||
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return wavDuration(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MP3 can start with ID3 or frame sync 0xFFEx
|
||||||
|
if string(head[0:3]) == "ID3" || (head[0] == 0xFF && (head[1]&0xE0) == 0xE0) {
|
||||||
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return mp3Duration(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, errors.New("unsupported audio format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioDurationFromURL downloads the url to a temp file and returns duration.
|
||||||
|
func AudioDurationFromURL(url string) (time.Duration, error) {
|
||||||
|
path, err := fetchURLToTemp(url, 30*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer os.Remove(path)
|
||||||
|
return AudioDuration(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoDurationMP4 returns duration of an MP4 file (MOV/MP4 base media).
|
||||||
|
func VideoDurationMP4(path string) (time.Duration, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return mp4Duration(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoDurationMP4FromURL downloads the url to a temp file and returns duration.
|
||||||
|
func VideoDurationMP4FromURL(url string) (time.Duration, error) {
|
||||||
|
path, err := fetchURLToTemp(url, 30*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer os.Remove(path)
|
||||||
|
return VideoDurationMP4(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- helpers ----------------------
|
||||||
|
|
||||||
|
func fetchURLToTemp(url string, timeout time.Duration) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("http status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := os.CreateTemp("", "media-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer tmp.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(tmp, resp.Body); err != nil {
|
||||||
|
path := tmp.Name()
|
||||||
|
_ = os.Remove(path)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tmp.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- WAV ----------------------
|
||||||
|
|
||||||
|
func wavDuration(r io.ReadSeeker) (time.Duration, error) {
|
||||||
|
// RIFF header already checked outside if needed. We parse chunks to get fmt and data.
|
||||||
|
// WAV little-endian
|
||||||
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read RIFF header (12 bytes)
|
||||||
|
head := make([]byte, 12)
|
||||||
|
if _, err := io.ReadFull(r, head); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if string(head[0:4]) != "RIFF" || string(head[8:12]) != "WAVE" {
|
||||||
|
return 0, errors.New("invalid wav header")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sampleRate uint32
|
||||||
|
var numChans uint16
|
||||||
|
var bitsPerSample uint16
|
||||||
|
var byteRate uint32
|
||||||
|
var dataSize uint32
|
||||||
|
|
||||||
|
for {
|
||||||
|
chunkHdr := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(r, chunkHdr); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
ckID := string(chunkHdr[0:4])
|
||||||
|
ckSize := binary.LittleEndian.Uint32(chunkHdr[4:8])
|
||||||
|
|
||||||
|
switch ckID {
|
||||||
|
case "fmt ":
|
||||||
|
fmtData := make([]byte, ckSize)
|
||||||
|
if _, err := io.ReadFull(r, fmtData); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// audioFormat := binary.LittleEndian.Uint16(fmtData[0:2]) // 1 = PCM
|
||||||
|
numChans = binary.LittleEndian.Uint16(fmtData[2:4])
|
||||||
|
sampleRate = binary.LittleEndian.Uint32(fmtData[4:8])
|
||||||
|
byteRate = binary.LittleEndian.Uint32(fmtData[8:12])
|
||||||
|
// blockAlign := binary.LittleEndian.Uint16(fmtData[12:14])
|
||||||
|
if len(fmtData) >= 16 {
|
||||||
|
bitsPerSample = binary.LittleEndian.Uint16(fmtData[14:16])
|
||||||
|
}
|
||||||
|
case "data":
|
||||||
|
dataSize = ckSize
|
||||||
|
// Skip data content
|
||||||
|
if _, err := r.Seek(int64(ckSize), io.SeekCurrent); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Skip other chunks
|
||||||
|
if _, err := r.Seek(int64(ckSize), io.SeekCurrent); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Chunks are word-aligned (pad byte if odd size)
|
||||||
|
if ckSize%2 == 1 {
|
||||||
|
if _, err := r.Seek(1, io.SeekCurrent); err != nil { // skip pad
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleRate == 0 || numChans == 0 {
|
||||||
|
return 0, errors.New("invalid wav fmt")
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationSeconds float64
|
||||||
|
if byteRate != 0 {
|
||||||
|
durationSeconds = float64(dataSize) / float64(byteRate)
|
||||||
|
} else {
|
||||||
|
bytesPerSec := float64(sampleRate) * float64(numChans) * float64(bitsPerSample) / 8.0
|
||||||
|
if bytesPerSec == 0 {
|
||||||
|
return 0, errors.New("invalid wav parameters")
|
||||||
|
}
|
||||||
|
durationSeconds = float64(dataSize) / bytesPerSec
|
||||||
|
}
|
||||||
|
return time.Duration(durationSeconds * float64(time.Second)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- MP3 ----------------------
|
||||||
|
|
||||||
|
func mp3Duration(r io.ReadSeeker) (time.Duration, error) {
|
||||||
|
// Strategy:
|
||||||
|
// 1) Skip ID3v2 header if present.
|
||||||
|
// 2) Try read first frame and detect XING/Info or VBRI to get total frames and duration.
|
||||||
|
// 3) If VBR headers not present, fall back to CBR estimation: (audioDataBytes * 8) / bitrate.
|
||||||
|
|
||||||
|
// File size
|
||||||
|
fi, err := fileSizeFromSeeker(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip ID3v2
|
||||||
|
var id3v2Size int64
|
||||||
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
id3v2Size, err = skipID3v2(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember audio start offset
|
||||||
|
startOffset, _ := r.Seek(0, io.SeekCurrent)
|
||||||
|
|
||||||
|
// Read first frame header (search sync)
|
||||||
|
off, fh, err := findNextMP3Frame(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err := r.Seek(off, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for XING/Info header in first frame (for VBR)
|
||||||
|
totalFrames, sampleRate, samplesPerFrame, bitrateKbps, vbrFound, err := parseFirstFrameForVBR(r, fh)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if vbrFound && totalFrames > 0 && sampleRate > 0 && samplesPerFrame > 0 {
|
||||||
|
seconds := (float64(totalFrames) * float64(samplesPerFrame)) / float64(sampleRate)
|
||||||
|
return time.Duration(seconds * float64(time.Second)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to CBR estimate using bitrate and data size (excluding ID3v2 and ID3v1)
|
||||||
|
// Detect ID3v1 at end (128 bytes TAG)
|
||||||
|
var id3v1Size int64
|
||||||
|
if fi >= 128 {
|
||||||
|
if _, err := r.Seek(fi-128, io.SeekStart); err == nil {
|
||||||
|
buf := make([]byte, 3)
|
||||||
|
if _, err := io.ReadFull(r, buf); err == nil {
|
||||||
|
if string(buf) == "TAG" {
|
||||||
|
id3v1Size = 128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audioBytes := fi - id3v2Size - id3v1Size - startOffset
|
||||||
|
if audioBytes <= 0 || bitrateKbps == 0 {
|
||||||
|
return 0, errors.New("unable to estimate mp3 duration")
|
||||||
|
}
|
||||||
|
// bitrateKbps in kbps, bytes -> bits
|
||||||
|
seconds := float64(audioBytes*8) / float64(bitrateKbps*1000)
|
||||||
|
return time.Duration(seconds * float64(time.Second)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mp3FrameHeader struct {
|
||||||
|
Version int // 1: MPEG1, 2: MPEG2, 25: MPEG2.5
|
||||||
|
Layer int // 1,2,3
|
||||||
|
BitrateKbps int
|
||||||
|
SampleRate int
|
||||||
|
Padding int
|
||||||
|
ChannelMode int // 0:Stereo,1:Joint,2:Dual,3:Mono
|
||||||
|
}
|
||||||
|
|
||||||
|
func findNextMP3Frame(r io.ReadSeeker) (int64, mp3FrameHeader, error) {
|
||||||
|
var hdr mp3FrameHeader
|
||||||
|
// Start from current pos and scan up to 64KB
|
||||||
|
start, _ := r.Seek(0, io.SeekCurrent)
|
||||||
|
limit := int64(64 * 1024)
|
||||||
|
buf := make([]byte, limit)
|
||||||
|
n, err := r.Read(buf)
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return 0, hdr, err
|
||||||
|
}
|
||||||
|
for i := 0; i+4 <= n; i++ {
|
||||||
|
if buf[i] == 0xFF && (buf[i+1]&0xE0) == 0xE0 { // sync
|
||||||
|
if h, ok := parseMP3Header(buf[i : i+4]); ok {
|
||||||
|
offset := start + int64(i)
|
||||||
|
return offset, h, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, hdr, errors.New("mp3 frame not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMP3Header(b []byte) (mp3FrameHeader, bool) {
|
||||||
|
var h mp3FrameHeader
|
||||||
|
if len(b) < 4 {
|
||||||
|
return h, false
|
||||||
|
}
|
||||||
|
if b[0] != 0xFF || (b[1]&0xE0) != 0xE0 {
|
||||||
|
return h, false
|
||||||
|
}
|
||||||
|
versionBits := (b[1] >> 3) & 0x03
|
||||||
|
layerBits := (b[1] >> 1) & 0x03
|
||||||
|
bitrateBits := (b[2] >> 4) & 0x0F
|
||||||
|
sampleRateBits := (b[2] >> 2) & 0x03
|
||||||
|
paddingBit := (b[2] >> 1) & 0x01
|
||||||
|
channelMode := (b[3] >> 6) & 0x03
|
||||||
|
|
||||||
|
var version int
|
||||||
|
switch versionBits {
|
||||||
|
case 0x00:
|
||||||
|
version = 25 // MPEG 2.5
|
||||||
|
case 0x02:
|
||||||
|
version = 2 // MPEG 2
|
||||||
|
case 0x03:
|
||||||
|
version = 1 // MPEG 1
|
||||||
|
default:
|
||||||
|
return h, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer int
|
||||||
|
switch layerBits {
|
||||||
|
case 0x01:
|
||||||
|
layer = 3
|
||||||
|
case 0x02:
|
||||||
|
layer = 2
|
||||||
|
case 0x03:
|
||||||
|
layer = 1
|
||||||
|
default:
|
||||||
|
return h, false
|
||||||
|
}
|
||||||
|
|
||||||
|
br := mp3BitrateKbps(version, layer, int(bitrateBits))
|
||||||
|
if br == 0 {
|
||||||
|
return h, false
|
||||||
|
}
|
||||||
|
sr := mp3SampleRate(version, int(sampleRateBits))
|
||||||
|
if sr == 0 {
|
||||||
|
return h, false
|
||||||
|
}
|
||||||
|
|
||||||
|
h = mp3FrameHeader{
|
||||||
|
Version: version,
|
||||||
|
Layer: layer,
|
||||||
|
BitrateKbps: br,
|
||||||
|
SampleRate: sr,
|
||||||
|
Padding: int(paddingBit),
|
||||||
|
ChannelMode: int(channelMode),
|
||||||
|
}
|
||||||
|
return h, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func mp3BitrateKbps(version, layer, index int) int {
|
||||||
|
// index: 1..14 valid; 0,15 invalid
|
||||||
|
if index <= 0 || index == 15 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Tables per ISO/IEC 11172-3/13818-3 (common subset)
|
||||||
|
var tbl [15]int
|
||||||
|
if layer == 1 { // Layer I
|
||||||
|
if version == 1 { // MPEG1
|
||||||
|
tbl = [15]int{0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}
|
||||||
|
} else { // MPEG2/2.5
|
||||||
|
tbl = [15]int{0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}
|
||||||
|
}
|
||||||
|
} else if layer == 2 { // Layer II
|
||||||
|
if version == 1 {
|
||||||
|
tbl = [15]int{0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}
|
||||||
|
} else {
|
||||||
|
tbl = [15]int{0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}
|
||||||
|
}
|
||||||
|
} else { // Layer III
|
||||||
|
if version == 1 {
|
||||||
|
tbl = [15]int{0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}
|
||||||
|
} else {
|
||||||
|
tbl = [15]int{0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tbl[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func mp3SampleRate(version, index int) int {
|
||||||
|
if index == 3 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// base table for MPEG1
|
||||||
|
base := [3]int{44100, 48000, 32000}
|
||||||
|
sr := base[index]
|
||||||
|
if version == 2 { // MPEG2
|
||||||
|
sr /= 2
|
||||||
|
} else if version == 25 { // MPEG2.5
|
||||||
|
sr /= 4
|
||||||
|
}
|
||||||
|
return sr
|
||||||
|
}
|
||||||
|
|
||||||
|
func samplesPerMP3Frame(version, layer int) int {
|
||||||
|
switch layer {
|
||||||
|
case 1:
|
||||||
|
return 384
|
||||||
|
case 2:
|
||||||
|
return 1152
|
||||||
|
case 3:
|
||||||
|
if version == 1 {
|
||||||
|
return 1152
|
||||||
|
}
|
||||||
|
return 576 // MPEG2/2.5 Layer III
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFirstFrameForVBR(r io.ReadSeeker, fh mp3FrameHeader) (totalFrames uint32, sampleRate int, samplesPerFrame int, bitrateKbps int, vbrFound bool, err error) {
|
||||||
|
// After the 4-byte header, possible side info and then XING/Info
|
||||||
|
if _, err = r.Seek(0, io.SeekCurrent); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Re-read header
|
||||||
|
hdr := make([]byte, 4)
|
||||||
|
if _, err = io.ReadFull(r, hdr); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// side info size depends on MPEG version and channel mode (for Layer III)
|
||||||
|
sideInfoSize := 0
|
||||||
|
if fh.Layer == 3 { // Layer III
|
||||||
|
if fh.Version == 1 { // MPEG1
|
||||||
|
if fh.ChannelMode == 3 { // mono
|
||||||
|
sideInfoSize = 17
|
||||||
|
} else {
|
||||||
|
sideInfoSize = 32
|
||||||
|
}
|
||||||
|
} else { // MPEG2/2.5
|
||||||
|
if fh.ChannelMode == 3 {
|
||||||
|
sideInfoSize = 9
|
||||||
|
} else {
|
||||||
|
sideInfoSize = 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read next up to 120 bytes to search for XING/Info or VBRI
|
||||||
|
buf := make([]byte, sideInfoSize+120)
|
||||||
|
if _, err = io.ReadFull(r, buf); err != nil {
|
||||||
|
// If short, still try within available
|
||||||
|
if !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search XING/Info signature
|
||||||
|
sigs := [][]byte{[]byte("Xing"), []byte("Info")}
|
||||||
|
for _, sig := range sigs {
|
||||||
|
idx := indexOf(buf, sig)
|
||||||
|
if idx >= 0 {
|
||||||
|
// flags after signature (4 bytes), then if frames flag set, 4 bytes frames
|
||||||
|
if len(buf) >= idx+4+4 {
|
||||||
|
flags := binary.BigEndian.Uint32(buf[idx+4 : idx+8])
|
||||||
|
var frames uint32
|
||||||
|
if (flags & 0x01) != 0 { // frames present
|
||||||
|
if len(buf) >= idx+8+4 {
|
||||||
|
frames = binary.BigEndian.Uint32(buf[idx+8 : idx+12])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if frames > 0 {
|
||||||
|
vbrFound = true
|
||||||
|
totalFrames = frames
|
||||||
|
sampleRate = fh.SampleRate
|
||||||
|
samplesPerFrame = samplesPerMP3Frame(fh.Version, fh.Layer)
|
||||||
|
bitrateKbps = fh.BitrateKbps
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check VBRI (usually at 32 bytes after header for MPEG1 Layer III)
|
||||||
|
if len(buf) >= 4 {
|
||||||
|
idx := indexOf(buf, []byte("VBRI"))
|
||||||
|
if idx >= 0 {
|
||||||
|
if len(buf) >= idx+4+2+2+4+4 {
|
||||||
|
// VBRI layout: 'VBRI'(4) + version(2) + delay(2) + quality(2?) varies; but at offset 10 comes bytes: bytes (4), frames (4)
|
||||||
|
// Some docs: offset 10: bytes, offset 14: frames (big-endian)
|
||||||
|
bytesOffset := idx + 10
|
||||||
|
framesOffset := idx + 14
|
||||||
|
if len(buf) >= framesOffset+4 {
|
||||||
|
frames := binary.BigEndian.Uint32(buf[framesOffset : framesOffset+4])
|
||||||
|
if frames > 0 {
|
||||||
|
vbrFound = true
|
||||||
|
totalFrames = frames
|
||||||
|
sampleRate = fh.SampleRate
|
||||||
|
samplesPerFrame = samplesPerMP3Frame(fh.Version, fh.Layer)
|
||||||
|
bitrateKbps = fh.BitrateKbps
|
||||||
|
_ = bytesOffset // not used
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No VBR header. Provide header info for CBR fallback
|
||||||
|
sampleRate = fh.SampleRate
|
||||||
|
samplesPerFrame = samplesPerMP3Frame(fh.Version, fh.Layer)
|
||||||
|
bitrateKbps = fh.BitrateKbps
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexOf(haystack []byte, needle []byte) int {
|
||||||
|
for i := 0; i+len(needle) <= len(haystack); i++ {
|
||||||
|
match := true
|
||||||
|
for j := 0; j < len(needle); j++ {
|
||||||
|
if haystack[i+j] != needle[j] {
|
||||||
|
match = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipID3v2(r io.ReadSeeker) (int64, error) {
|
||||||
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
head := make([]byte, 10)
|
||||||
|
if _, err := io.ReadFull(r, head); err != nil {
|
||||||
|
return 0, nil // no header
|
||||||
|
}
|
||||||
|
if string(head[0:3]) != "ID3" {
|
||||||
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
// size: 4 synchsafe bytes
|
||||||
|
sz := int64((int(head[6]&0x7F) << 21) | (int(head[7]&0x7F) << 14) | (int(head[8]&0x7F) << 7) | int(head[9]&0x7F))
|
||||||
|
// total header size = 10 + sz (+ footer 10 if flag set)
|
||||||
|
footer := int64(0)
|
||||||
|
if (head[5] & 0x10) != 0 { // footer present
|
||||||
|
footer = 10
|
||||||
|
}
|
||||||
|
total := 10 + sz + footer
|
||||||
|
if _, err := r.Seek(total, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileSizeFromSeeker(r io.ReadSeeker) (int64, error) {
|
||||||
|
cur, err := r.Seek(0, io.SeekCurrent)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
end, err := r.Seek(0, io.SeekEnd)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err := r.Seek(cur, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return end, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- MP4 ----------------------
|
||||||
|
|
||||||
|
type mp4BoxHeader struct {
|
||||||
|
Size uint64
|
||||||
|
Type [4]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBoxHeader(r io.ReadSeeker) (mp4BoxHeader, error) {
|
||||||
|
var h mp4BoxHeader
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(r, buf); err != nil {
|
||||||
|
return h, err
|
||||||
|
}
|
||||||
|
sz := binary.BigEndian.Uint32(buf[0:4])
|
||||||
|
copy(h.Type[:], buf[4:8])
|
||||||
|
if sz == 1 {
|
||||||
|
// 64-bit size follows
|
||||||
|
ext := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(r, ext); err != nil {
|
||||||
|
return h, err
|
||||||
|
}
|
||||||
|
h.Size = binary.BigEndian.Uint64(ext)
|
||||||
|
} else {
|
||||||
|
h.Size = uint64(sz)
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipBox(r io.ReadSeeker, boxSize uint64, alreadyRead int64) error {
|
||||||
|
toSkip := int64(boxSize) - alreadyRead
|
||||||
|
if toSkip < 0 {
|
||||||
|
return fmt.Errorf("invalid box size")
|
||||||
|
}
|
||||||
|
_, err := r.Seek(toSkip, io.SeekCurrent)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mp4Duration(r io.ReadSeeker) (time.Duration, error) {
|
||||||
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Find moov box
|
||||||
|
var moovStart int64
|
||||||
|
var moovSize uint64
|
||||||
|
for {
|
||||||
|
pos, _ := r.Seek(0, io.SeekCurrent)
|
||||||
|
h, err := readBoxHeader(r)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if string(h.Type[:]) == "moov" {
|
||||||
|
moovStart = pos
|
||||||
|
moovSize = h.Size
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if h.Size < 8 {
|
||||||
|
return 0, errors.New("invalid mp4 box size")
|
||||||
|
}
|
||||||
|
if err := skipBox(r, h.Size, 8); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if moovStart == 0 && moovSize == 0 {
|
||||||
|
return 0, errors.New("moov not found")
|
||||||
|
}
|
||||||
|
// Parse inside moov for video trak mdhd, else mvhd
|
||||||
|
if _, err := r.Seek(moovStart+8, io.SeekStart); err != nil { // skip moov header
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
end := moovStart + int64(moovSize)
|
||||||
|
var movieTimescale uint32
|
||||||
|
var movieDuration uint64
|
||||||
|
var foundVideoMdhd bool
|
||||||
|
var mdhdTimescale uint32
|
||||||
|
var mdhdDuration uint64
|
||||||
|
|
||||||
|
for {
|
||||||
|
pos, _ := r.Seek(0, io.SeekCurrent)
|
||||||
|
if pos >= end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h, err := readBoxHeader(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch string(h.Type[:]) {
|
||||||
|
case "mvhd":
|
||||||
|
// movie header
|
||||||
|
ver := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(r, ver); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err := r.Seek(3, io.SeekCurrent); err != nil { // flags
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if ver[0] == 1 {
|
||||||
|
// version 1: 64-bit duration
|
||||||
|
buf := make([]byte, 8+8+4+8) // ctime(8) mtime(8) timescale(4) duration(8)
|
||||||
|
if _, err := io.ReadFull(r, buf); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
movieTimescale = binary.BigEndian.Uint32(buf[16:20])
|
||||||
|
movieDuration = binary.BigEndian.Uint64(buf[20:28])
|
||||||
|
} else {
|
||||||
|
buf := make([]byte, 4+4+4+4) // ctime mtime timescale duration
|
||||||
|
if _, err := io.ReadFull(r, buf); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
movieTimescale = binary.BigEndian.Uint32(buf[8:12])
|
||||||
|
movieDuration = uint64(binary.BigEndian.Uint32(buf[12:16]))
|
||||||
|
}
|
||||||
|
// skip rest of mvhd
|
||||||
|
read := int64(1 + 3)
|
||||||
|
if ver[0] == 1 {
|
||||||
|
read += int64(8 + 8 + 4 + 8)
|
||||||
|
} else {
|
||||||
|
read += int64(4 + 4 + 4 + 4)
|
||||||
|
}
|
||||||
|
if err := skipBox(r, h.Size, 8+read); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
case "trak":
|
||||||
|
// parse trak for hdlr and mdhd
|
||||||
|
tEnd := int64(0)
|
||||||
|
if h.Size < 8 {
|
||||||
|
return 0, errors.New("invalid trak size")
|
||||||
|
}
|
||||||
|
tEnd = pos + int64(h.Size)
|
||||||
|
var isVideo bool
|
||||||
|
var tMdhdTimescale uint32
|
||||||
|
var tMdhdDuration uint64
|
||||||
|
for {
|
||||||
|
cpos, _ := r.Seek(0, io.SeekCurrent)
|
||||||
|
if cpos >= tEnd {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ch, err := readBoxHeader(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch string(ch.Type[:]) {
|
||||||
|
case "mdia":
|
||||||
|
mEnd := cpos + int64(ch.Size)
|
||||||
|
for {
|
||||||
|
mpos, _ := r.Seek(0, io.SeekCurrent)
|
||||||
|
if mpos >= mEnd {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
mh, err := readBoxHeader(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch string(mh.Type[:]) {
|
||||||
|
case "hdlr":
|
||||||
|
// skip version+flags (4), pre_defined(4)
|
||||||
|
if _, err := r.Seek(8, io.SeekCurrent); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
handler := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(r, handler); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if string(handler) == "vide" {
|
||||||
|
isVideo = true
|
||||||
|
}
|
||||||
|
if err := skipBox(r, mh.Size, 8+8+4); err != nil { // header + skipped + read handler
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
case "mdhd":
|
||||||
|
ver := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(r, ver); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err := r.Seek(3, io.SeekCurrent); err != nil { // flags
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if ver[0] == 1 {
|
||||||
|
buf := make([]byte, 8+8+4+8)
|
||||||
|
if _, err := io.ReadFull(r, buf); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
tMdhdTimescale = binary.BigEndian.Uint32(buf[16:20])
|
||||||
|
tMdhdDuration = binary.BigEndian.Uint64(buf[20:28])
|
||||||
|
} else {
|
||||||
|
buf := make([]byte, 4+4+4+4)
|
||||||
|
if _, err := io.ReadFull(r, buf); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
tMdhdTimescale = binary.BigEndian.Uint32(buf[8:12])
|
||||||
|
tMdhdDuration = uint64(binary.BigEndian.Uint32(buf[12:16]))
|
||||||
|
}
|
||||||
|
if err := skipBox(r, mh.Size, 8+1+3+int64(lenVersionPayload(ver[0]))); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := skipBox(r, mh.Size, 8); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := skipBox(r, ch.Size, 8); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isVideo && tMdhdTimescale != 0 && tMdhdDuration != 0 {
|
||||||
|
foundVideoMdhd = true
|
||||||
|
mdhdTimescale = tMdhdTimescale
|
||||||
|
mdhdDuration = tMdhdDuration
|
||||||
|
}
|
||||||
|
// Skip remaining of trak if any
|
||||||
|
if _, err := r.Seek(tEnd, io.SeekStart); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := skipBox(r, h.Size, 8); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundVideoMdhd && mdhdTimescale != 0 {
|
||||||
|
sec := float64(mdhdDuration) / float64(mdhdTimescale)
|
||||||
|
return time.Duration(sec * float64(time.Second)), nil
|
||||||
|
}
|
||||||
|
if movieTimescale != 0 {
|
||||||
|
sec := float64(movieDuration) / float64(movieTimescale)
|
||||||
|
return time.Duration(sec * float64(time.Second)), nil
|
||||||
|
}
|
||||||
|
return 0, errors.New("failed to read mp4 duration")
|
||||||
|
}
|
||||||
|
|
||||||
|
func lenVersionPayload(ver byte) int {
|
||||||
|
if ver == 1 {
|
||||||
|
return 8 + 8 + 4 + 8
|
||||||
|
}
|
||||||
|
return 4 + 4 + 4 + 4
|
||||||
|
}
|
||||||
@@ -101,3 +101,16 @@ func GetImgExt(filename string) string {
|
|||||||
}
|
}
|
||||||
return ext
|
return ext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFileSize(url string) (int64, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int64(len(body)), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ npm run build
|
|||||||
cd ../build
|
cd ../build
|
||||||
|
|
||||||
# remove docker image if exists
|
# remove docker image if exists
|
||||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-api:$version-$arch
|
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:$version-$arch
|
||||||
# build docker image for Geek-AI API
|
# build docker image for Geek-AI API
|
||||||
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-api:$version-$arch -f dockerfile-api-go ../
|
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:$version-$arch -f dockerfile-api-go ../
|
||||||
|
|
||||||
# build docker image for Geek-AI-web
|
# build docker image for Geek-AI-web
|
||||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-web:$version-$arch
|
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:$version-$arch
|
||||||
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-web:$version-$arch -f dockerfile-web-$arch ../
|
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:$version-$arch -f dockerfile-web-$arch ../
|
||||||
|
|
||||||
if [ "$3" = "push" ];then
|
if [ "$3" = "push" ];then
|
||||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-api:$version-$arch
|
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:$version-$arch
|
||||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-web:$version-$arch
|
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:$version-$arch
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ SET time_zone = "+00:00";
|
|||||||
/*!40101 SET NAMES utf8mb4 */;
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- 数据库: `geekai_plus`
|
-- 数据库: `geekai`
|
||||||
--
|
--
|
||||||
CREATE DATABASE IF NOT EXISTS `geekai_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
CREATE DATABASE IF NOT EXISTS `geekai` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||||
USE `geekai_plus`;
|
USE `geekai`;
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'websocket' upgrade;
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
# listen 443 ssl;
|
||||||
|
listen 8080;
|
||||||
|
# server_name www.chatgpt.com; #替换成你自己的域名
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
# 日志地址
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
|
||||||
|
index index.html;
|
||||||
|
root /var/www/app/dist; # 这里改成前端静态页面的地址
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
|
# 后端 API 的转发
|
||||||
|
location /api/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_connect_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 12s;
|
||||||
|
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://geekai-api:5678;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 静态资源转发
|
||||||
|
location /static/ {
|
||||||
|
proxy_pass http://geekai-api:5678;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
# mysql
|
||||||
|
geekai-mysql:
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/mysql:8.0.33
|
||||||
|
container_name: geekai-mysql
|
||||||
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=mhSCk0NheGhmtsha
|
||||||
|
ports:
|
||||||
|
- '3307:3306'
|
||||||
|
volumes:
|
||||||
|
- ./conf/mysql/my.cnf:/etc/mysql/my.cnf
|
||||||
|
- ./data/mysql/data:/var/lib/mysql
|
||||||
|
- ./logs/mysql:/var/log/mysql
|
||||||
|
- ./data/mysql/init.d:/docker-entrypoint-initdb.d/
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
# redis
|
||||||
|
geekai-redis:
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/redis:6.0.6
|
||||||
|
restart: always
|
||||||
|
container_name: geekai-redis
|
||||||
|
command: redis-server --requirepass mhSCk0NheGhmtsha
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
ports:
|
||||||
|
- '6380:6379'
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 3s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
geekai-tika:
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/tika:latest
|
||||||
|
container_name: geekai-tika
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '9999:9998'
|
||||||
|
|
||||||
|
# 后端 API 程序
|
||||||
|
geekai-api:
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.2.8-amd64
|
||||||
|
container_name: geekai-api
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
geekai-mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
geekai-redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- DEBUG=false
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- CONFIG_FILE=config.toml
|
||||||
|
ports:
|
||||||
|
- '5678:5678'
|
||||||
|
volumes:
|
||||||
|
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
|
||||||
|
- ./conf/config.toml:/var/www/app/config.toml
|
||||||
|
- ./logs/app:/var/www/app/logs
|
||||||
|
- ./static:/var/www/app/static
|
||||||
|
- ./data/leveldb:/var/www/app/data
|
||||||
|
|
||||||
|
# 前端应用
|
||||||
|
geekai-web:
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.2.8-amd64
|
||||||
|
container_name: geekai-web
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- geekai-api
|
||||||
|
ports:
|
||||||
|
- '8080:8080'
|
||||||
|
volumes:
|
||||||
|
- ./logs/nginx:/var/log/nginx
|
||||||
|
- ./conf/nginx/conf.d:/etc/nginx/conf.d
|
||||||
|
- ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
- ./conf/nginx/ssl:/etc/nginx/ssl
|
||||||
@@ -6,7 +6,7 @@ VITE_ADMIN_USER=admin
|
|||||||
VITE_ADMIN_PASS=admin123
|
VITE_ADMIN_PASS=admin123
|
||||||
VITE_KEY_PREFIX=GeekAI_DEV_
|
VITE_KEY_PREFIX=GeekAI_DEV_
|
||||||
VITE_TITLE="Geek-AI 创作系统"
|
VITE_TITLE="Geek-AI 创作系统"
|
||||||
VITE_VERSION=v4.2.6
|
VITE_VERSION=v4.2.8
|
||||||
VITE_DOCS_URL=https://docs.geekai.me
|
VITE_DOCS_URL=https://docs.geekai.me
|
||||||
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||||
VITE_GITEE_URL=https://gitee.com/blackfox/geekai
|
VITE_GITEE_URL=https://gitee.com/blackfox/geekai
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
VITE_API_HOST=
|
VITE_API_HOST=
|
||||||
VITE_WS_HOST=
|
VITE_WS_HOST=
|
||||||
VITE_KEY_PREFIX=GeekAI_
|
VITE_KEY_PREFIX=GeekAI_
|
||||||
VITE_VERSION=v4.2.6
|
VITE_VERSION=v4.2.8
|
||||||
|
VITE_TITLE="Geek-AI 创作系统"
|
||||||
VITE_DOCS_URL=https://docs.geekai.me
|
VITE_DOCS_URL=https://docs.geekai.me
|
||||||
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||||
VITE_GITEE_URL=https://gitee.com/blackfox/geekai
|
VITE_GITEE_URL=https://gitee.com/blackfox/geekai
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.el-input__wrapper {
|
.el-input__wrapper {
|
||||||
background: var(--card-bg);
|
//background: var(--card-bg);
|
||||||
}
|
}
|
||||||
.el-dialog__title {
|
.el-dialog__title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -138,7 +138,8 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tag, .el-tag.el-tag--primary {
|
.el-tag,
|
||||||
|
.el-tag.el-tag--primary {
|
||||||
--el-tag-bg-color: #f0ebff;
|
--el-tag-bg-color: #f0ebff;
|
||||||
}
|
}
|
||||||
.box-card {
|
.box-card {
|
||||||
@@ -148,4 +149,4 @@
|
|||||||
}
|
}
|
||||||
.el-table th.el-table__cell {
|
.el-table th.el-table__cell {
|
||||||
background-color: var(--chat-bg);
|
background-color: var(--chat-bg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,16 +49,17 @@
|
|||||||
|
|
||||||
.category-btn {
|
.category-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 15px 10px;
|
padding: 10px 15px;
|
||||||
border: 2px solid var(--border-color, #f0f0f0);
|
border: 2px solid var(--border-color, #f0f0f0);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
background: var(--card-bg-secondary, #fafafa);
|
background: var(--card-bg-secondary, #fafafa);
|
||||||
/* 暗色主题支持 */
|
/* 暗色主题支持 */
|
||||||
[data-theme="dark"] & {
|
[data-theme='dark'] & {
|
||||||
background: var(--card-bg-secondary-dark, #23242a);
|
background: var(--card-bg-secondary-dark, #23242a);
|
||||||
border-color: var(--border-color-dark, #33343a);
|
border-color: var(--border-color-dark, #33343a);
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--primary-color, #5865f2);
|
border-color: var(--primary-color, #5865f2);
|
||||||
background: var(--card-bg-hover, #f8f9ff);
|
background: var(--card-bg-hover, #f8f9ff);
|
||||||
[data-theme="dark"] & {
|
[data-theme='dark'] & {
|
||||||
background: var(--card-bg-hover-dark, #2a2b31);
|
background: var(--card-bg-hover-dark, #2a2b31);
|
||||||
}
|
}
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
@@ -76,8 +77,11 @@
|
|||||||
border-color: var(--primary-color, #5865f2);
|
border-color: var(--primary-color, #5865f2);
|
||||||
background: var(--primary-gradient, linear-gradient(135deg, #5865f2 0%, #7289da 100%));
|
background: var(--primary-gradient, linear-gradient(135deg, #5865f2 0%, #7289da 100%));
|
||||||
color: var(--primary-text-on-primary, #fff);
|
color: var(--primary-text-on-primary, #fff);
|
||||||
[data-theme="dark"] & {
|
[data-theme='dark'] & {
|
||||||
background: var(--primary-gradient-dark, linear-gradient(135deg, #23242a 0%, #2a2b31 100%));
|
background: var(
|
||||||
|
--primary-gradient-dark,
|
||||||
|
linear-gradient(135deg, #23242a 0%, #2a2b31 100%)
|
||||||
|
);
|
||||||
color: var(--primary-text-on-primary-dark, #fff);
|
color: var(--primary-text-on-primary-dark, #fff);
|
||||||
}
|
}
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
@@ -96,108 +100,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 功能开关
|
|
||||||
.function-switch {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
|
|
||||||
.switch-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-theme-color);
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
color: var(--primary-color, #5865f2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 5px 15px;
|
|
||||||
border: 1px solid var(--border-color, #e0e0e0);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--card-bg-secondary, #f9f9f9);
|
|
||||||
[data-theme="dark"] & {
|
|
||||||
background: var(--card-bg-secondary-dark, #23242a);
|
|
||||||
border-color: var(--border-color-dark, #33343a);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-info {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.switch-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-theme-color);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-desc {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-sub-color, #666);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 参数容器
|
|
||||||
.params-container {
|
|
||||||
.function-panel {
|
|
||||||
.param-line {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
|
|
||||||
&.pt {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-theme-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-right: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-theme-color);
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-info {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 15px;
|
|
||||||
background: var(--info-bg, #f0f8ff);
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid var(--primary-color, #5865f2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
margin-top: 30px;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 右侧主要内容区域
|
// 右侧主要内容区域
|
||||||
@@ -239,6 +141,23 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 4px 24px rgba(88, 101, 242, 0.12);
|
box-shadow: 0 4px 24px rgba(88, 101, 242, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 增强任务项悬停动画
|
||||||
|
transition: box-shadow 3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.5s;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1.5px solid transparent;
|
||||||
|
border-radius: 12px;
|
||||||
|
// background: #fff;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18), 0 1.5px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #a259ff;
|
||||||
|
transform: scale(1.025) translateY(-2px);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
.task-left {
|
.task-left {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -253,18 +172,69 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
.preview-image, .preview-video {
|
.preview-image,
|
||||||
|
.preview-video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视频预览包装器
|
||||||
|
.preview-video-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.video-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .video-mask {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 3;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.preview-placeholder {
|
.preview-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--text-disabled-color, #999);
|
color: var(--text-disabled-color, #999);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
.el-icon, .iconfont {
|
.el-icon,
|
||||||
|
.iconfont {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
@@ -346,4 +316,75 @@
|
|||||||
max-height: 220px;
|
max-height: 220px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 错误信息样式
|
||||||
|
.err-msg-clip {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板选择器样式
|
||||||
|
.jimeng-template-select {
|
||||||
|
.el-select-dropdown__item {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提示词指南样式
|
||||||
|
.prompt-guide {
|
||||||
|
margin: 12px 0 16px;
|
||||||
|
background-color: var(--el-fill-color-blank);
|
||||||
|
|
||||||
|
.el-collapse {
|
||||||
|
--el-collapse-border-color: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-content {
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-section {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-subtitle {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 18px;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-left: 3px solid #a3a3a3;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -189,10 +189,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 4px solid #42b983;
|
border-left: 5px solid #c1c1c1;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
color: #777;
|
color: #777;
|
||||||
background-color: rgba(66, 185, 131, 0.1);
|
background-color: rgba(193, 193, 193, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
|||||||
@@ -887,3 +887,284 @@
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark 主题样式 - 按照 theme-dark.scss 的模式 */
|
||||||
|
:root[data-theme='dark'] .jimeng-create {
|
||||||
|
background-color: rgb(13, 20, 53);
|
||||||
|
|
||||||
|
/* 页面头部样式 */
|
||||||
|
.sticky {
|
||||||
|
background-color: rgb(31, 41, 55) !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: rgb(75, 85, 99) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 功能分类选择 */
|
||||||
|
.jimeng-create__content {
|
||||||
|
.bg-white {
|
||||||
|
background-color: rgb(55, 65, 81) !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-700 {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-900 {
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-600 {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-500 {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray-100:hover {
|
||||||
|
background-color: rgb(75, 85, 99) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 组件样式覆盖 */
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
background-color: rgb(31, 41, 55) !important;
|
||||||
|
border-color: rgb(75, 85, 99) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner::placeholder) {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner::placeholder) {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-switch__core) {
|
||||||
|
background-color: rgb(75, 85, 99) !important;
|
||||||
|
border-color: rgb(75, 85, 99) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-switch.is-checked .el-switch__core) {
|
||||||
|
background-color: rgb(139, 92, 246) !important;
|
||||||
|
border-color: rgb(139, 92, 246) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-slider__runway) {
|
||||||
|
background-color: rgb(75, 85, 99) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-slider__bar) {
|
||||||
|
background-color: rgb(139, 92, 246) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-slider__button) {
|
||||||
|
border-color: rgb(139, 92, 246) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tooltip__trigger) {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交按钮 */
|
||||||
|
.bg-gradient-to-r {
|
||||||
|
background: linear-gradient(88deg, #af61f0 1.44%, #5b62ce) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(88deg, #9f51e0 1.44%, #4b52be) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: linear-gradient(88deg, #6b7280 1.44%, #4b5563) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 作品列表 */
|
||||||
|
.jimeng-create__works {
|
||||||
|
&-title {
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
background-color: rgb(55, 65, 81) !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
.jimeng-create__works-item-info {
|
||||||
|
&-title {
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-prompt {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tags {
|
||||||
|
&-item {
|
||||||
|
background-color: rgb(75, 85, 99) !important;
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
background-color: rgb(239, 68, 68) !important;
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background-color: rgb(59, 130, 246) !important;
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--power {
|
||||||
|
background-color: rgb(139, 92, 246) !important;
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-quick-actions {
|
||||||
|
button {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-error {
|
||||||
|
&-content {
|
||||||
|
background-color: rgb(31, 41, 55) !important;
|
||||||
|
border-color: rgb(239, 68, 68) !important;
|
||||||
|
|
||||||
|
.jimeng-create__works-item-error-text {
|
||||||
|
color: rgb(239, 68, 68) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jimeng-create__works-item-error-copy-btn {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-loading {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-finished {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 媒体预览弹窗 */
|
||||||
|
.jimeng-create__media-dialog {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
background-color: rgb(55, 65, 81) !important;
|
||||||
|
box-shadow: 0 0 15px rgba(107, 80, 225, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
background-color: rgb(31, 41, 55) !important;
|
||||||
|
border-bottom-color: rgb(75, 85, 99) !important;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片上传组件 */
|
||||||
|
:deep(.image-upload) {
|
||||||
|
.upload-area {
|
||||||
|
background-color: rgb(31, 41, 55) !important;
|
||||||
|
border-color: rgb(75, 85, 99) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgb(139, 92, 246) !important;
|
||||||
|
background-color: rgb(55, 65, 81) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
color: rgb(139, 92, 246) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义选择组件 */
|
||||||
|
:deep(.custom-select) {
|
||||||
|
.select-trigger {
|
||||||
|
background-color: rgb(31, 41, 55) !important;
|
||||||
|
border-color: rgb(75, 85, 99) !important;
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-dropdown {
|
||||||
|
background-color: rgb(55, 65, 81) !important;
|
||||||
|
border-color: rgb(75, 85, 99) !important;
|
||||||
|
box-shadow: 0 0 15px rgba(107, 80, 225, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option {
|
||||||
|
color: rgb(209, 213, 219) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(75, 85, 99) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: rgb(139, 92, 246) !important;
|
||||||
|
color: rgb(255, 255, 255) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态组件 */
|
||||||
|
:deep(.van-empty) {
|
||||||
|
.van-empty__description {
|
||||||
|
color: rgb(156, 163, 175) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
|
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
|
||||||
--border-active: rgba(255, 255, 255, 0.1);
|
--border-active: rgba(255, 255, 255, 0.1);
|
||||||
--card-bg: #252d58;
|
--card-bg: #252d58;
|
||||||
|
--card-bg-secondary: #313a6b;
|
||||||
--chat-bg: #1f243f;
|
--chat-bg: #1f243f;
|
||||||
--chat-wel-bg: #2d2f38;
|
--chat-wel-bg: #2d2f38;
|
||||||
--card-bg-table: rgba(17, 28, 68, 1);
|
--card-bg-table: rgba(17, 28, 68, 1);
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
|
--btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
|
||||||
--border-active: rgba(134, 140, 255, 1);
|
--border-active: rgba(134, 140, 255, 1);
|
||||||
--code-btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
|
--code-btnColor: linear-gradient(88deg, #af61f0 1.44%, #5b62ce);
|
||||||
--card-bg: #fff;
|
--card-bg: #f5f5f5;
|
||||||
|
--card-bg-secondary: #e5e5e5;
|
||||||
--chat-bg: #fff;
|
--chat-bg: #fff;
|
||||||
--theme-bg: linear-gradient(88deg, #fff3f3 1.44%, #e7e8ff);
|
--theme-bg: linear-gradient(88deg, #fff3f3 1.44%, #e7e8ff);
|
||||||
--theme-bg-all: #f5f7fd;
|
--theme-bg-all: #f5f7fd;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-textarea__inner) {
|
:deep(.el-textarea__inner) {
|
||||||
background: transparent;
|
// background: transparent;
|
||||||
color: var(--text-theme-color);
|
color: var(--text-theme-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,8 @@
|
|||||||
color: var(--text-theme-color);
|
color: var(--text-theme-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-input, .el-slider {
|
.el-input,
|
||||||
|
.el-slider {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +423,8 @@
|
|||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|
||||||
.prompt, .failed {
|
.prompt,
|
||||||
|
.failed {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
max-height: 80px;
|
max-height: 80px;
|
||||||
@@ -547,7 +549,8 @@
|
|||||||
.left .container {
|
.left .container {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
|
|
||||||
.video, .el-image {
|
.video,
|
||||||
|
.el-image {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -562,4 +565,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 4125778 */
|
font-family: "iconfont"; /* Project id 4125778 */
|
||||||
src: url('iconfont.woff2?t=1756954977612') format('woff2'),
|
src: url('iconfont.woff2?t=1757571432313') format('woff2'),
|
||||||
url('iconfont.woff?t=1756954977612') format('woff'),
|
url('iconfont.woff?t=1757571432313') format('woff'),
|
||||||
url('iconfont.ttf?t=1756954977612') format('truetype');
|
url('iconfont.ttf?t=1757571432313') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,42 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-resize:before {
|
||||||
|
content: "\e718";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-template:before {
|
||||||
|
content: "\e8a6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-error-line:before {
|
||||||
|
content: "\e868";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-success-line:before {
|
||||||
|
content: "\e88c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-yunjing:before {
|
||||||
|
content: "\e69b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-action:before {
|
||||||
|
content: "\e658";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dancing:before {
|
||||||
|
content: "\e659";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-running:before {
|
||||||
|
content: "\e65e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-shuziren:before {
|
||||||
|
content: "\e6df";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-cube:before {
|
.icon-cube:before {
|
||||||
content: "\e72c";
|
content: "\e72c";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,69 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "10564356",
|
||||||
|
"name": "resize",
|
||||||
|
"font_class": "resize",
|
||||||
|
"unicode": "e718",
|
||||||
|
"unicode_decimal": 59160
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "1727381",
|
||||||
|
"name": "34模板、框架",
|
||||||
|
"font_class": "template",
|
||||||
|
"unicode": "e8a6",
|
||||||
|
"unicode_decimal": 59558
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "9626841",
|
||||||
|
"name": "错误",
|
||||||
|
"font_class": "error-line",
|
||||||
|
"unicode": "e868",
|
||||||
|
"unicode_decimal": 59496
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "9626990",
|
||||||
|
"name": "正确",
|
||||||
|
"font_class": "success-line",
|
||||||
|
"unicode": "e88c",
|
||||||
|
"unicode_decimal": 59532
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "40613765",
|
||||||
|
"name": "运镜控制",
|
||||||
|
"font_class": "yunjing",
|
||||||
|
"unicode": "e69b",
|
||||||
|
"unicode_decimal": 59035
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "5215282",
|
||||||
|
"name": "动作",
|
||||||
|
"font_class": "action",
|
||||||
|
"unicode": "e658",
|
||||||
|
"unicode_decimal": 58968
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7250581",
|
||||||
|
"name": "跳舞",
|
||||||
|
"font_class": "dancing",
|
||||||
|
"unicode": "e659",
|
||||||
|
"unicode_decimal": 58969
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "8153037",
|
||||||
|
"name": "动作",
|
||||||
|
"font_class": "running",
|
||||||
|
"unicode": "e65e",
|
||||||
|
"unicode_decimal": 58974
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "42680536",
|
||||||
|
"name": "数字人",
|
||||||
|
"font_class": "shuziren",
|
||||||
|
"unicode": "e6df",
|
||||||
|
"unicode_decimal": 59103
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "544492",
|
"icon_id": "544492",
|
||||||
"name": "cube",
|
"name": "cube",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 8.5 KiB |