mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-10-31 14:23:43 +08:00 
			
		
		
		
	Compare commits
	
		
			946 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5ba2e000a1 | ||
|  | 805c2a045e | ||
|  | 1b1c327c35 | ||
|  | 7d0a05ee11 | ||
|  | 825a1b1027 | ||
|  | 950e7d1b00 | ||
|  | d8f9f48278 | ||
|  | bdd76addf3 | ||
|  | 2936f21f12 | ||
|  | 705ca4d20a | ||
|  | 6a066f1b8e | ||
|  | b7d137247a | ||
|  | 4373642ebd | ||
|  | 5bf90920f5 | ||
|  | edcbb3e226 | ||
|  | 5213bdf08b | ||
|  | cf817fd8ea | ||
|  | a2481ff1cf | ||
|  | bc7d06d3e5 | ||
|  | 8e81dfa12a | ||
|  | 0ff76f0f21 | ||
|  | 787caa84c8 | ||
|  | c2503e663a | ||
|  | 405a88862b | ||
|  | 296eabe09a | ||
|  | d30d5585c6 | ||
|  | 54b45ec2ff | ||
|  | c434f85045 | ||
|  | 4d10279870 | ||
|  | 6aaf607ed7 | ||
|  | cff0397735 | ||
|  | 2aa0b51c09 | ||
|  | ce8a2d0222 | ||
|  | 135755d21d | ||
|  | 5be4e83876 | ||
|  | 9de9489673 | ||
|  | cbc9eb3a59 | ||
|  | 0593359ef8 | ||
|  | 9814fec930 | ||
|  | 53ba731159 | ||
|  | 2081d3ce29 | ||
|  | 41d9c097e8 | ||
|  | 1fe1e40a43 | ||
|  | ad6e2dd370 | ||
|  | bb63f23414 | ||
|  | 43f6bf74f2 | ||
|  | 662d7b099e | ||
|  | d5eeeea764 | ||
|  | 43c507c597 | ||
|  | e356771049 | ||
|  | 48139290ed | ||
|  | bd852c82b7 | ||
|  | 13564993d7 | ||
|  | bfc1e1bc2c | ||
|  | ba20717a09 | ||
|  | 52e40daf23 | ||
|  | b2f57aa483 | ||
|  | 4c2dba1004 | ||
|  | 430a7b2297 | ||
|  | c91a38a882 | ||
|  | 6e02bee4b7 | ||
|  | b62218110e | ||
|  | e2960b2607 | ||
|  | 88e7c39066 | ||
|  | 2a6dd636fa | ||
|  | 6bf38f78d5 | ||
|  | 5a04a935be | ||
|  | 8923e938d2 | ||
|  | 1a1734abf0 | ||
|  | 8093a3eeb2 | ||
|  | 9edb3d0a82 | ||
|  | d95fab11be | ||
|  | 6ef09c8ad5 | ||
|  | 283a023a06 | ||
|  | d315edef5f | ||
|  | 5fa17b300e | ||
|  | 32919de7a7 | ||
|  | 7d126aab41 | ||
|  | 16ac57ced3 | ||
|  | 4976b967e7 | ||
|  | e874178782 | ||
|  | 8cb66ad01b | ||
|  | f887a39912 | ||
|  | 2beffd3dd3 | ||
|  | d8cb92d8d4 | ||
|  | 158db83965 | ||
|  | 603bfa7def | ||
|  | 829fb879a6 | ||
|  | 0385e60ce1 | ||
|  | aaea23f785 | ||
|  | 131efd6ba5 | ||
|  | 866564370d | ||
|  | dcdc0d8918 | ||
|  | 6c7fa17e50 | ||
|  | 38a0d00142 | ||
|  | 5c77e67b0f | ||
|  | 961cee5e41 | ||
|  | c9cc93be8c | ||
|  | 49f2e1a71e | ||
|  | 97eff6085a | ||
|  | 8b2e2d61af | ||
|  | c096efb416 | ||
|  | cdaf6fb9dc | ||
|  | 78f443ed6d | ||
|  | 54e8d72b10 | ||
|  | 05161f48fd | ||
|  | e971bf6b88 | ||
|  | 55b979784c | ||
|  | 79adc871ef | ||
|  | 97aa922b5f | ||
|  | 11c760a4e8 | ||
|  | 8144fada25 | ||
|  | 87b03332d9 | ||
|  | 8b14eeadf4 | ||
|  | e0ead127e0 | ||
|  | 0887bcdee0 | ||
|  | 67d83041d7 | ||
|  | 1350f388f0 | ||
|  | 65dde9e69d | ||
|  | 2e5bd238b7 | ||
|  | 8fc8fd6cba | ||
|  | dfc6c87250 | ||
|  | b63e01225e | ||
|  | 561b82027a | ||
|  | f6d8fbf570 | ||
|  | 568201ebbb | ||
|  | ab421f2185 | ||
|  | f71a2f5263 | ||
|  | d000cc5a67 | ||
|  | 04d6ba0853 | ||
|  | 8d7c028ca8 | ||
|  | 3ae7ebfeaf | ||
|  | aa42d38387 | ||
|  | 43843b92f2 | ||
|  | 5da879600a | ||
|  | 87ed2064e3 | ||
|  | 34e96e91d4 | ||
|  | 8c4c2b89ce | ||
|  | 373021c191 | ||
|  | 740c3c1b00 | ||
|  | 67c7132e6b | ||
|  | c77843424b | ||
|  | 2d4959aa7d | ||
|  | 167c59a159 | ||
|  | 1d0006ce59 | ||
|  | 6a8b4ee2f1 | ||
|  | 72b1515b68 | ||
|  | 754ba02263 | ||
|  | 7ddf57ae06 | ||
|  | 3f0252b498 | ||
|  | cc5180a6f7 | ||
|  | 1d9d487f0e | ||
|  | 96f1126d02 | ||
|  | 7f9b8d8246 | ||
|  | 5132d52a44 | ||
|  | 1bcbf74883 | ||
|  | abdf5298fe | ||
|  | 2129f7a8b7 | ||
|  | f6f8748521 | ||
|  | 59301df073 | ||
|  | e17dcf4d5f | ||
|  | 09f44e6d9b | ||
|  | 59824bffc5 | ||
|  | cb0dacd5e0 | ||
|  | 7463cfc66c | ||
|  | b248560ba2 | ||
|  | 37368fe13f | ||
|  | 246b023624 | ||
|  | 9f44c34d34 | ||
|  | a6b9f57a50 | ||
|  | 42bc23cacf | ||
|  | 282f55c7a3 | ||
|  | 44798f89ba | ||
|  | 596cb2b206 | ||
|  | d1965deff1 | ||
|  | b793b81768 | ||
|  | a5ef4299ec | ||
|  | cdb1a8bde1 | ||
|  | 233f6e00f0 | ||
|  | b7dba68549 | ||
|  | 64e5fc48ba | ||
|  | a692cf1338 | ||
|  | 6998dd7af4 | ||
|  | 9343c73e0f | ||
|  | 739cd46539 | ||
|  | f8fed83507 | ||
|  | d63536d5ef | ||
|  | 4905fb28d4 | ||
|  | a3a2a8abcb | ||
|  | 839dd8dbf4 | ||
|  | 0375164f40 | ||
|  | 691294b444 | ||
|  | bdea12c51a | ||
|  | a27d9ea259 | ||
|  | 7cd824c284 | ||
|  | e27d95e2b5 | ||
|  | c24b4d7074 | ||
|  | 6839827db0 | ||
|  | ab24398748 | ||
|  | 6110522b54 | ||
|  | bcdf5e3776 | ||
|  | 2207830db9 | ||
|  | d52dfbfef4 | ||
|  | d6a04f96fe | ||
|  | 66ccb387e8 | ||
|  | 5f820b9dc1 | ||
|  | 3cc2263dc7 | ||
|  | f0a3c5d8ae | ||
|  | 2a4ef27774 | ||
|  | 2b057f32aa | ||
|  | bc6451026f | ||
|  | 99fd596862 | ||
|  | f0959b5df6 | ||
|  | 6788edbe9d | ||
|  | 3895305882 | ||
|  | 1b0938b33f | ||
|  | c2acbaaa94 | ||
|  | 02faff461a | ||
|  | e18e5a38c6 | ||
|  | 2f9b1b7835 | ||
|  | 717b137a6d | ||
|  | f755bdccae | ||
|  | 4bba77ab47 | ||
|  | 6944a32ff3 | ||
|  | 5742b40aee | ||
|  | 7f1ec90748 | ||
|  | 4a99be2f15 | ||
|  | bee19392c1 | ||
|  | 27c816cf3b | ||
|  | 0d81776212 | ||
|  | 00d31a2379 | ||
|  | cccab31c0f | ||
|  | 5d65505ab7 | ||
|  | 3dc7d0516a | ||
|  | 50335ebc2d | ||
|  | bcadee7290 | ||
|  | cac3194d5b | ||
|  | 4ddf3bf2bf | ||
|  | d45f9fbad6 | ||
|  | d98b08d7cd | ||
|  | 5a8fe5a6cf | ||
|  | 36c27d6092 | ||
|  | 3ab29da8f0 | ||
|  | 3699f024f1 | ||
|  | 3d37a3d367 | ||
|  | 73d8236697 | ||
|  | 114d0088dc | ||
|  | 43b6665370 | ||
|  | 5fb9f84182 | ||
|  | e35c34ad9a | ||
|  | 1a4d798f8b | ||
|  | afb91a7023 | ||
|  | dc4c1f7877 | ||
|  | bbc8fe2b40 | ||
|  | 3c34e8e0e7 | ||
|  | 57c932f07c | ||
|  | 922202734a | ||
|  | 8b3b0139b0 | ||
|  | 31828a3336 | ||
|  | b270960a04 | ||
|  | 5c4899df6e | ||
|  | 9a797bb4a5 | ||
|  | b0c9ffc5a6 | ||
|  | f527cc5b98 | ||
|  | debe8dc209 | ||
|  | 2f0215ac87 | ||
|  | dd5cc206e5 | ||
|  | 142cd553a3 | ||
|  | 657ecccee3 | ||
|  | 1232c3cd9c | ||
|  | 3ac04a3938 | ||
|  | b7abc42209 | ||
|  | a48179ce0e | ||
|  | e589f25a05 | ||
|  | cc1a3ce343 | ||
|  | 7bb76d581c | ||
|  | 0d733c0be0 | ||
|  | 8b40ac5b5c | ||
|  | 24479814e9 | ||
|  | 99df028237 | ||
|  | b354b88876 | ||
|  | 5e0be4d10e | ||
|  | 468b48151f | ||
|  | fa5c036041 | ||
|  | 0fdc588167 | ||
|  | 2e023cb8dc | ||
|  | e933f32d9c | ||
|  | bd4b0c4d65 | ||
|  | 0b2501c1d8 | ||
|  | 9d28e62142 | ||
|  | c1d892069e | ||
|  | 61b2dbc9f1 | ||
|  | be3245666e | ||
|  | dacdd6fe74 | ||
|  | 6807f7e88a | ||
|  | 087f5ab2d1 | ||
|  | 47c5a0387b | ||
|  | f9da18ad52 | ||
|  | 5c9025ca22 | ||
|  | d02cb573fd | ||
|  | caa538a1d0 | ||
|  | b584b4bfb6 | ||
|  | bda335212d | ||
|  | 06f4cdc649 | ||
|  | 336a7d5b56 | ||
|  | a0f464830f | ||
|  | 9bf7fa4081 | ||
|  | 96ead65774 | ||
|  | 7ad41927aa | ||
|  | 4ca9dfd9c0 | ||
|  | 8a9f386d8f | ||
|  | adfee8bf58 | ||
|  | fbfa2a71a9 | ||
|  | 9a1368ef17 | ||
|  | 31b02b97d3 | ||
|  | 42da38c5c3 | ||
|  | 0a01b55713 | ||
|  | 3b292c2a12 | ||
|  | db0ba0d9a0 | ||
|  | 3a23ff6b42 | ||
|  | 1e9c5adb0a | ||
|  | abab76ccc6 | ||
|  | 6efd92806f | ||
|  | cfe333e89f | ||
|  | a7237fe62f | ||
|  | c3c454b7d7 | ||
|  | d4d708d44b | ||
|  | 7f0b6a3a46 | ||
|  | c2a7c089d2 | ||
|  | df5bd4df60 | ||
|  | 79b6010104 | ||
|  | 97b0a98793 | ||
|  | 5230f90540 | ||
|  | 803db4e895 | ||
|  | 7cee9f2ebb | ||
|  | 8be9a21efd | ||
|  | 6a3e26b566 | ||
|  | 0355c37bef | ||
|  | 9b7ee538c4 | ||
|  | d900a3d08e | ||
|  | cdf5b66729 | ||
|  | 1cff4b63cd | ||
|  | da14309ef9 | ||
|  | fbb216fe3b | ||
|  | 95efbd5659 | ||
|  | 4596c1049c | ||
|  | b35d95f0c7 | ||
|  | 01419df998 | ||
|  | a6c00c42fa | ||
|  | 4cc9db7115 | ||
|  | 4f1ed54059 | ||
|  | 8227a73e35 | ||
|  | adfd8c1939 | ||
|  | 8eed7ff534 | ||
|  | c79c4e74d0 | ||
|  | f1855fd0a1 | ||
|  | 1f964c74e9 | ||
|  | 4fb2c5803c | ||
|  | b5947545cb | ||
|  | 342b76f666 | ||
|  | 49b5906bc7 | ||
|  | 3075bfb7fc | ||
|  | 82e06fad33 | ||
|  | 4a9028747b | ||
|  | 4a8ff0ccf0 | ||
|  | 99341f0484 | ||
|  | f58ac29ad0 | ||
|  | 7060edb3e5 | ||
|  | 41ae411f9b | ||
|  | 79b7fee47c | ||
|  | 0044bf10af | ||
|  | e9348d3611 | ||
|  | b9236e09a7 | ||
|  | 09b38d5f42 | ||
|  | 7bb539a06e | ||
|  | 5cdada8265 | ||
|  | 4147c217b1 | ||
|  | 8dda639b23 | ||
|  | 8487d2c9eb | ||
|  | c5e583b215 | ||
|  | 549f618cff | ||
|  | e9a3510346 | ||
|  | 30e6e963b3 | ||
|  | c72d963f45 | ||
|  | 172d498618 | ||
|  | 313993532e | ||
|  | e53db3582c | ||
|  | 72c6bd3f77 | ||
|  | ca8b349df3 | ||
|  | 1b206c3640 | ||
|  | c60276fc9f | ||
|  | d00a3167c0 | ||
|  | 6b1cd8c30c | ||
|  | 46f12dc9ad | ||
|  | a3e1d8ae21 | ||
|  | 72a066b93e | ||
|  | 0327a829ac | ||
|  | 882e9b8819 | ||
|  | ef58cfadaa | ||
|  | bf958d6113 | ||
|  | 71611273d7 | ||
|  | b27c654311 | ||
|  | 90930ea9f9 | ||
|  | 1ab2185ff1 | ||
|  | 0f2f978d4c | ||
|  | f61963b0b0 | ||
|  | 2aa413960d | ||
|  | aa4bbba5ec | ||
|  | eba61fea2d | ||
|  | 34e3455128 | ||
|  | 07dca3e739 | ||
|  | 4cb4b145f9 | ||
|  | 1ed417cb69 | ||
|  | 6cf91a84ca | ||
|  | 0b566980fc | ||
|  | f86176b342 | ||
|  | c700b32670 | ||
|  | 22641b452a | ||
|  | d3fbb8c19e | ||
|  | e3bb69ff10 | ||
|  | 770360c614 | ||
|  | f302a0478f | ||
|  | a88697b43a | ||
|  | cc6f140812 | ||
|  | 424f2b3bdc | ||
|  | ec0c13a600 | ||
|  | a1f03bec4c | ||
|  | b5bd4a5e0e | ||
|  | 7c2e49bfdb | ||
|  | f80fe6d041 | ||
|  | 72f80a96bc | ||
|  | 2de655a1cf | ||
|  | da2bd4a501 | ||
|  | e0aa62c40d | ||
|  | 9d26a892d1 | ||
|  | 4ece7f2847 | ||
|  | 32368caf1b | ||
|  | e91f54e79e | ||
|  | bb8f4c57c4 | ||
|  | 43bfac99b6 | ||
|  | be379b6d63 | ||
|  | 17f3c9b840 | ||
|  | 24de97fac2 | ||
|  | bf27b44fee | ||
|  | 1802b4fe4d | ||
|  | 241a5c7bc9 | ||
|  | 557d547bf1 | ||
|  | 2e7b75affb | ||
|  | bc21a1d443 | ||
|  | 3fc9e10a24 | ||
|  | 5fa1aa2060 | ||
|  | be8a0ec184 | ||
|  | b02e3aad95 | ||
|  | 08eca511ad | ||
|  | c34e911596 | ||
|  | 8a452c3072 | ||
|  | 13bfb14107 | ||
|  | 4188b0969e | ||
|  | 0c27795a10 | ||
|  | d05693c5c1 | ||
|  | c0b2063b38 | ||
|  | 4d183747b1 | ||
|  | 08fe1b2f75 | ||
|  | db3e8a267e | ||
|  | 8fc62682c4 | ||
|  | 75031914a3 | ||
|  | a4c9fdd95a | ||
|  | 6a9bfeb5aa | ||
|  | e654766f60 | ||
|  | 0ef6955f96 | ||
|  | b4501557c9 | ||
|  | a2ed99e6cb | ||
|  | 6bd6bb3885 | ||
|  | 399cf65fc9 | ||
|  | 24906a6df1 | ||
|  | d772bbebe6 | ||
|  | 14988853a3 | ||
|  | 7b3f16ac9f | ||
|  | 82b2755c18 | ||
|  | ff4b267858 | ||
|  | a590d0497f | ||
|  | ac30d906f0 | ||
|  | 5bc071e038 | ||
|  | 88b956cf98 | ||
|  | f725cf4661 | ||
|  | 057cc1e8a6 | ||
|  | de122735b8 | ||
|  | e87ede981c | ||
|  | 606fb498e1 | ||
|  | a0c06e40a4 | ||
|  | aba8f57279 | ||
|  | 960286a350 | ||
|  | 8c93fa51f6 | ||
|  | cb0e7d64ff | ||
|  | 8e7413da97 | ||
|  | a36f14eb94 | ||
|  | f2f9f6e488 | ||
|  | 85068b8ca2 | ||
|  | f2cfcfeefc | ||
|  | 755273a898 | ||
|  | d4a24a0f1d | ||
|  | 92281fcbb7 | ||
|  | 636db4afcc | ||
|  | ba25b8755e | ||
|  | 6399d13a49 | ||
|  | 06fa54fd25 | ||
|  | a335b965d0 | ||
|  | 725adaa7d0 | ||
|  | 7e7e81e974 | ||
|  | 8cfe6bfc17 | ||
|  | 33de83f2ac | ||
|  | 3f856afec8 | ||
|  | 4e4dc4cb73 | ||
|  | 02a9c422fe | ||
|  | ca69341024 | ||
|  | 169bf069ce | ||
|  | 1bee0ab04d | ||
|  | 440d91dd0e | ||
|  | 8168e246a8 | ||
|  | 2ef07574ae | ||
|  | 37392f2bb2 | ||
|  | a80cd3848e | ||
|  | db6ed84451 | ||
|  | 4463cc5963 | ||
|  | d316158fe2 | ||
|  | e02a8d7586 | ||
|  | 9988dff885 | ||
|  | 35ef5674ff | ||
|  | 976da45bce | ||
|  | c83ac48bd2 | ||
|  | 3d159a833e | ||
|  | 4b09878bdd | ||
|  | b0162e6a92 | ||
|  | 8ab15e5dc4 | ||
|  | d2ac807252 | ||
|  | 0af01f6f1f | ||
|  | 013b319fab | ||
|  | 2899ba5949 | ||
|  | a558b7e104 | ||
|  | 7a833e2233 | ||
|  | bf65746d00 | ||
|  | f08a7862de | ||
|  | 023a2c2f09 | ||
|  | 1bcd0f4c1a | ||
|  | a0f3bc8ccb | ||
|  | dea72738c1 | ||
|  | a1d1fe7763 | ||
|  | a39ed9764c | ||
|  | aaa5ba99aa | ||
|  | 2113508b6d | ||
|  | 7fe4212684 | ||
|  | 8bdda64794 | ||
|  | ec08c24dca | ||
|  | a992a5b3b3 | ||
|  | 0f05970141 | ||
|  | e5e762efcd | ||
|  | b3d0c1ef9c | ||
|  | 397078f7ff | ||
|  | 3ad8065e20 | ||
|  | 66c7717f04 | ||
|  | 412f8ecc6c | ||
|  | 51dcf642b3 | ||
|  | bfeea555b2 | ||
|  | 479f94c372 | ||
|  | 0140713e86 | ||
|  | 15b2ec9721 | ||
|  | c9cd082855 | ||
|  | d7c002890c | ||
|  | 348dd22279 | ||
|  | 3e99b4cbf6 | ||
|  | 6968da3ac7 | ||
|  | bf1c1b84c3 | ||
|  | c70314d930 | ||
|  | 9104ca8e49 | ||
|  | 2af33b3630 | ||
|  | 654e795545 | ||
|  | c62ba2451e | ||
|  | d72d1b8a99 | ||
|  | b939d6016b | ||
|  | 36a2626ccc | ||
|  | bd057a4cc9 | ||
|  | dc24a8c781 | ||
|  | 59fa21779b | ||
|  | a140671aad | ||
|  | 5fe8990fb4 | ||
|  | 12799b7159 | ||
|  | 9929746b1d | ||
|  | d70035ff0c | ||
|  | eec90274d8 | ||
|  | e8fff55c42 | ||
|  | 3cf3cdd705 | ||
|  | 9801fce659 | ||
|  | 4c1f51110b | ||
|  | 913d538587 | ||
|  | 9e704365fc | ||
|  | 485bdbc56a | ||
|  | 7000168fd4 | ||
|  | 5694f97a6b | ||
|  | b677d3fac7 | ||
|  | dc6719cf54 | ||
|  | 7de5b55091 | ||
|  | 76c5101092 | ||
|  | 2f8d2f4854 | ||
|  | b1ee34ba0c | ||
|  | 069ad6a09a | ||
|  | bf1403c818 | ||
|  | bcc622a24d | ||
|  | a06a81a415 | ||
|  | d1950acd01 | ||
|  | 039b70eed2 | ||
|  | d8e4308b1b | ||
|  | 434fbb3463 | ||
|  | de3eb8969c | ||
|  | fbd6eac877 | ||
|  | 1fecab177b | ||
|  | b1b385c455 | ||
|  | 3c6e86d04b | ||
|  | 3d2035d08a | ||
|  | da86f916d8 | ||
|  | e7a07f7e92 | ||
|  | b01e6387fc | ||
|  | d86aca0f5d | ||
|  | 09414fe36a | ||
|  | df0e7508db | ||
|  | 92b1f01118 | ||
|  | 8fb8bd932b | ||
|  | 3f74b94784 | ||
|  | e9467341fa | ||
|  | 131e051ddc | ||
|  | f626fe3166 | ||
|  | 6bc57b6132 | ||
|  | d972e97c88 | ||
|  | 3991f4daec | ||
|  | f6b567d6fc | ||
|  | 8addba8203 | ||
|  | 3ab930a107 | ||
|  | de512a5ea2 | ||
|  | 113cfae2dc | ||
|  | 33aebf9cb5 | ||
|  | 6e58ddf681 | ||
|  | cae5c049e4 | ||
|  | ff76e4bd89 | ||
|  | a0a506a3c4 | ||
|  | aa5a4a9977 | ||
|  | abf4f061c1 | ||
|  | 245cd3ee1a | ||
|  | 45cb29d9a0 | ||
|  | d974b1ff0e | ||
|  | 56269170cb | ||
|  | 4290c4ca22 | ||
|  | 7f7c8e831e | ||
|  | 8f057ca9d1 | ||
|  | 4a56621ec3 | ||
|  | a398e7a550 | ||
|  | 96816c12ca | ||
|  | 9984926f69 | ||
|  | a2a6081027 | ||
|  | 5a10ed37a7 | ||
|  | 1a9dd9de0b | ||
|  | 0dae5bef71 | ||
|  | b4413ed726 | ||
|  | 5e1fe88b8b | ||
|  | 91ed41b536 | ||
|  | 024c0032eb | ||
|  | 4a9f7e3bce | ||
|  | cf4dcc34ec | ||
|  | 4d612c15af | ||
|  | 8aec87cc02 | ||
|  | 442e411cde | ||
|  | acec0194de | ||
|  | 8557f5b94a | ||
|  | babef8baae | ||
|  | efd4ab46f5 | ||
|  | ae8239e5de | ||
|  | f0994ba457 | ||
|  | dae91ed243 | ||
|  | de42a428e6 | ||
|  | 63c7041e1f | ||
|  | b1263ddc69 | ||
|  | 7e50e17aaf | ||
|  | a7265c4251 | ||
|  | 6f39f639bd | ||
|  | a7db123437 | ||
|  | 241c714a8b | ||
|  | 67ac3cfe32 | ||
|  | c926e0afcc | ||
|  | 5bc07e6d57 | ||
|  | c3666a9a71 | ||
|  | 23b5ffa97d | ||
|  | a2c7a75705 | ||
|  | d68f2ef12c | ||
|  | 67d30353f0 | ||
|  | 4813163eac | ||
|  | 5c5210625e | ||
|  | a4a1eec30b | ||
|  | d35164506a | ||
|  | 1ed08f01ea | ||
|  | eca07ab830 | ||
|  | 3512715704 | ||
|  | 6d07881141 | ||
|  | 251fe626f2 | ||
|  | 5fee3a9288 | ||
|  | 9b68d8101e | ||
|  | cfe6f27d48 | ||
|  | b314dd0900 | ||
|  | 950fab6374 | ||
|  | 9d1f5c42ce | ||
|  | a84046390b | ||
|  | aa29323a8a | ||
|  | d5617b7c3a | ||
|  | 1ef60a9e5e | ||
|  | fb6e395ad8 | ||
|  | d9216060bc | ||
|  | bcaa9a92e5 | ||
|  | 576adc9036 | ||
|  | 00de18be9a | ||
|  | c61d32816a | ||
|  | f3fbb0b89c | ||
|  | e311a39632 | ||
|  | 51407abe44 | ||
|  | 8a470b1038 | ||
|  | baddabaa16 | ||
|  | 427b434ce3 | ||
|  | 5f921965e6 | ||
|  | 1e705c8ed5 | ||
|  | b8ae65bb30 | ||
|  | 321e2087ea | ||
|  | aac60edce2 | ||
|  | 9dc9a6923e | ||
|  | 7ca4dfe09b | ||
|  | c584b82ddb | ||
|  | 5f17ab2501 | ||
|  | c84e912dd8 | ||
|  | 2ebff2623f | ||
|  | 72418ce4d7 | ||
|  | e221b1eed4 | ||
|  | 696306f066 | ||
|  | 1807d5b5d4 | ||
|  | 85c12aa322 | ||
|  | da9d0dc3bc | ||
|  | daaca822ac | ||
|  | 59ced3f947 | ||
|  | 22ae7dd1f3 | ||
|  | 1816e9d5cf | ||
|  | 9be6755f65 | ||
|  | e5fb986463 | ||
|  | 55d24e577e | ||
|  | 7f50fa3fcf | ||
|  | fff8b78aba | ||
|  | 7ca1989d98 | ||
|  | 4595dcb7ed | ||
|  | a688d3feb5 | ||
|  | 7d1d88a32f | ||
|  | d95c048edd | ||
|  | df2fc9d77c | ||
|  | d7e815d2bb | ||
|  | f58b0a65f0 | ||
|  | b59ad521ca | ||
|  | b47ff975b0 | ||
|  | d043a87b30 | ||
|  | 4cae7525d9 | ||
|  | 76966d2ce7 | ||
|  | 5a740aecb0 | ||
|  | 1ae79331e7 | ||
|  | 8b14e141d0 | ||
|  | 9cbc6c91c4 | ||
|  | 21c3a419a5 | ||
|  | 287fac3a89 | ||
|  | ba206bb387 | ||
|  | 4fc01f3f7b | ||
|  | f5ed71bcc6 | ||
|  | 8fc26183e9 | ||
|  | e8ae8fddb7 | ||
|  | b876867297 | ||
|  | 91dfd59731 | ||
|  | 5fdff90a10 | ||
|  | 96c62619e6 | ||
|  | 083155413d | ||
|  | d83019cbe4 | ||
|  | cc7271aa73 | ||
|  | f873d6b375 | ||
|  | c86169022a | ||
|  | db0a79da93 | ||
|  | 48393e0e83 | ||
|  | 7b4730271d | ||
|  | 9cbe36d4c6 | ||
|  | b25bb2cc53 | ||
|  | 79ded6018b | ||
|  | 59f316b341 | ||
|  | f307b8ba7a | ||
|  | 5034a20345 | ||
|  | 26944f9e39 | ||
|  | e64946c3b6 | ||
|  | e0a62d9b35 | ||
|  | 39dbffd8d0 | ||
|  | 952d6183ed | ||
|  | 3365a6008d | ||
|  | 2e13ddf405 | ||
|  | 1d3acc8ed3 | ||
|  | fa341bab30 | ||
|  | 036a6e3e41 | ||
|  | f4c6ca4554 | ||
|  | 327929243c | ||
|  | f4349c7a8c | ||
|  | 4b46d847f0 | ||
|  | c3f016eae8 | ||
|  | ebd3ef842f | ||
|  | 18c033d57f | ||
|  | b676f80110 | ||
|  | f7fbaa534d | ||
|  | ea93a22e14 | ||
|  | 9f7e6778c5 | ||
|  | 6c31a2bfa6 | ||
|  | f943669e18 | ||
|  | 3b26735998 | ||
|  | 79d25769ee | ||
|  | 1dd6800987 | ||
|  | 5e673a9ee0 | ||
|  | 92eb67a2af | ||
|  | b1bed59be2 | ||
|  | 0ac732a3a3 | ||
|  | bf3f68fa19 | ||
|  | 46a551df16 | ||
|  | 20a12462b1 | ||
|  | a49fb1940e | ||
|  | 32774d23c7 | ||
|  | 7ecd7eeba1 | ||
|  | 0cc9cf8b45 | ||
|  | d06f94bddd | ||
|  | b5955f08c9 | ||
|  | c120569894 | ||
|  | aa376f1737 | ||
|  | 0f8a0f89e3 | ||
|  | 68dc261b44 | ||
|  | 4cf3af0c7b | ||
|  | b99b6735d9 | ||
|  | 52189b7880 | ||
|  | 3dbeb1ccb6 | ||
|  | 5a0f272fa8 | ||
|  | 6561b99f8f | ||
|  | 329e3eee21 | ||
|  | 07049c9afb | ||
|  | 36c5dd7eaa | ||
|  | b84039b506 | ||
|  | fab43097dc | ||
|  | c8998ba294 | ||
|  | 40b2466adc | ||
|  | 35fedbe817 | ||
|  | 827acdd3f9 | ||
|  | 6c76086916 | ||
|  | 373370fde5 | ||
|  | 2165ba3406 | ||
|  | b0e02b43fc | ||
|  | 2107c13b3d | ||
|  | 5f41aecc8d | ||
|  | 6840a13370 | ||
|  | 8f1e28c0ab | ||
|  | 7903eed284 | ||
|  | 0d49ea0d41 | ||
|  | 2ee4db5e48 | ||
|  | 48c4789505 | ||
|  | 4e65a5b1a1 | ||
|  | b09d23f97f | ||
|  | 3529649ba9 | ||
|  | fdd659f393 | ||
|  | 9eb8da2789 | ||
|  | ffb1ef0470 | ||
|  | 862c6aea43 | ||
|  | 54fe4b7588 | ||
|  | c6062ee70e | ||
|  | bed184dc1f | ||
|  | 29094ba3b3 | ||
|  | a18188876c | ||
|  | 4faee3e48e | ||
|  | 1a6afcd266 | ||
|  | f567831d92 | ||
|  | cf36ca4285 | ||
|  | 0e4ae01498 | ||
|  | 7b90f8cb13 | ||
|  | c33215529a | ||
|  | c5be114db2 | ||
|  | cab955c292 | ||
|  | ca8c8e6490 | ||
|  | 253951b4b3 | ||
|  | 4d6444ebf3 | ||
|  | 94d8d8a9d4 | ||
|  | e02badf7bb | ||
|  | dd88622c64 | ||
|  | c4d7126c4d | ||
|  | 86bc063941 | ||
|  | dce85eb519 | ||
|  | 4ab879d697 | ||
|  | 681e52df50 | ||
|  | fb554c0315 | ||
|  | accf8eeb77 | ||
|  | 3e41edd3b5 | ||
|  | 9126cfff20 | ||
|  | 9806d5ff4c | ||
|  | d1d13a72e4 | ||
|  | 00c520d066 | ||
|  | 797ff66474 | ||
|  | 9d51a478b9 | ||
|  | 1d4179df75 | ||
|  | 917b6012e8 | ||
|  | da14632794 | ||
|  | a868a8a8b7 | ||
|  | 5037df744f | ||
|  | da88a501ad | ||
|  | b9885e8de4 | ||
|  | 22efe81080 | ||
|  | 2926717aef | ||
|  | a49d54d66c | ||
|  | ce0267e25b | ||
|  | 9088d22a66 | ||
|  | 1ff32d5d0a | ||
|  | adf6916598 | ||
|  | 31c14bf748 | ||
|  | 5395385d1e | ||
|  | 0035da548b | ||
|  | 9bceaade05 | ||
|  | 3194becdad | ||
|  | 6174b17c24 | ||
|  | 53fa4a20e9 | ||
|  | 43c1de51f5 | ||
|  | 7eb8c5ec35 | ||
|  | 296bf63196 | ||
|  | 6c65a21692 | ||
|  | daf83cfc84 | ||
|  | 871f5d39e4 | ||
|  | 3f91f37aff | ||
|  | a08981f876 | ||
|  | 5187a43543 | ||
|  | 6a733de556 | ||
|  | b9e9eae93f | ||
|  | 811f12135a | ||
|  | 2c172c0851 | ||
|  | 399a16fa28 | ||
|  | d971e95900 | ||
|  | 0b6940b121 | ||
|  | ad0f96fcb1 | ||
|  | 063b5655f7 | ||
|  | d03ed6570b | ||
|  | 1795a891ce | ||
|  | 05bdd81646 | ||
|  | cba54be913 | ||
|  | da0acfe851 | 
							
								
								
									
										6
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| deploy | ||||
| docs | ||||
| api/static  | ||||
| web/node_modules | ||||
| desktop | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/1.bug.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/1.bug.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| name: Bug 报告 🐛 | ||||
| description: 为 chatgpt-plus 提交错误报告 | ||||
| description: 为 geekai 提交错误报告 | ||||
| labels: ['Bug'] | ||||
| body: | ||||
|   - type: checkboxes | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/2.feature.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/2.feature.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| name: 功能优化 🚀 | ||||
| description: 为 chatgpt-plus 提交优化建议 | ||||
| description: 为 geekai 提交优化建议 | ||||
| labels: ['feature'] | ||||
| body: | ||||
|   - type: checkboxes | ||||
|   | ||||
							
								
								
									
										485
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										485
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,485 @@ | ||||
| # 更新日志 | ||||
| ## v4.1.7 | ||||
| * Bug修复:手机邮箱相关的注册问题 [#IB0HS5](https://gitee.com/blackfox/geekai/issues/IB0HS5) | ||||
| * Bug修复:音乐视频无法下载,思维导图下载后看不清文字[#IB0N2E](https://gitee.com/blackfox/geekai/issues/IB0N2E) | ||||
| * 功能优化:保存所有AIGC任务的原始信息,程序启动之后自动将未执行的任务加入到 redis 队列 | ||||
| * 功能优化:失败的任务自动退回算力,而不需要在删除的时候再退回 | ||||
| * 功能新增:支持设置一个专门的模型来翻译提示词,提供 Mate 提示词生成功能 | ||||
| * Bug修复:修复图片对话的时候,上下文不起作用的Bug | ||||
| * 功能新增:管理后台新增批量导出兑换码功能 | ||||
|  | ||||
| ## v4.1.6 | ||||
| * 功能新增:**支持OpenAI实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。 | ||||
| * 功能优化:优化MysQL容器配置文档,解决MysQL容器资源占用过高问题 | ||||
| * 功能新增:管理后台增加AI绘图任务管理,可在管理后台浏览和删除用户的绘图任务 | ||||
| * 功能新增:管理后台增加Suno和Luma任务管理功能 | ||||
| * Bug修复:修复管理后台删除兑换码报 404 错误 | ||||
| * 功能优化:优化充值产品定价逻辑,可以设置原价和优惠价,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**。 | ||||
|  | ||||
| ## v4.1.5 | ||||
| * 功能优化:重构 websocket 组件,减少 websocket 连接数,全站共享一个 websocket 连接 | ||||
| * Bug修复:兼容手机端原生微信支付和支付宝支付渠道 | ||||
| * Bug修复:修复删除绘图任务时候因为字段长度过短导致SQL执行失败问题 | ||||
| * 功能优化:优化 Vue 组件通信代码,使用共享数据来替换之前的事件订阅模式,效率更高一些 | ||||
| * 功能优化:优化思维导图生成功果页面,优化用户体验 | ||||
|  | ||||
| ## v4.1.4 | ||||
| * 功能优化:用户文件列表组件增加分页功能支持 | ||||
| * Bug修复:修复用户注册失败Bug,注册操作只弹出一次行为验证码 | ||||
| * 功能优化:首次登录不需要验证码,直接登录,登录失败之后才弹出验证码 | ||||
| * 功能新增:给 AI 应用(角色)增加分类,前端支持分类筛选 | ||||
| * 功能优化:允许用户在聊天页面设置是否使用流式输出或者一次性输出,兼容 GPT-O1 模型。 | ||||
| * 功能优化:移除PayJS支付渠道支持,PayJs已经关闭注册服务,请使用其他支付方式。 | ||||
| * 功能新增:新增GeeK易支付支付渠道,支持支付宝,微信支付,QQ钱包,京东支付,抖音支付,Paypal支付等支付方式 | ||||
| * Bug修复:修复注册页面 tab 组件没有自动选中问题 [#6](https://github.com/yangjian102621/geekai-plus/issues/6) | ||||
| * 功能优化:Luma生成视频任务增加自动翻译功能 | ||||
| * Bug修复:Suno 和 Luma 任务没有判断用户算力 | ||||
| * 功能新增:邮箱注册增加邮箱后缀白名单,防止使用某些垃圾邮箱注册薅羊毛 | ||||
| * 功能优化:清空未支付订单时,只清空超过15分钟未支付的订单 | ||||
|  | ||||
| ## v4.1.3 | ||||
| * 功能优化:重构用户登录模块,给所有的登录组件增加行为验证码功能,支持用户绑定手机,邮箱和微信 | ||||
| * 功能优化:重构找回密码模块,支持通过手机或者邮箱找回密码 | ||||
| * 功能优化:管理后台给可以拖动排序的组件添加拖动图标 | ||||
| * 功能优化:Suno 支持合成完整歌曲,和上传自己的音乐作品进行二次创作 | ||||
| * Bug修复:手机端角色和模型选择不生效 | ||||
| * Bug修复:用户登录过期之后聊天页面出现大量报错,需要刷新页面才能正常 | ||||
| * 功能优化:优化聊天页面 Websocket 断线重连代码,提高用户体验 | ||||
| * 功能优化:给算力增减服务全部加上数据库事务和同步锁 | ||||
| * 功能优化:支持用户在前端对话界面选择插件 | ||||
| * 功能新增:支持 Luma 文生视频功能 | ||||
|  | ||||
| ## v4.1.2 | ||||
| * Bug修复:修复思维导图页面获取模型失败的问题 | ||||
| * 功能优化:优化MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力 | ||||
| * Bug修复:修复后台拖动排序组件 Bug | ||||
| * 功能优化:更新数据库失败时候显示具体的的报错信息 | ||||
| * Bug修复:修复管理后台对话详情页内容显示异常问题 | ||||
| * 功能优化:管理后台新增清空所有未支付订单的功能 | ||||
| * 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求 | ||||
| * 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力 | ||||
|  | ||||
| ## v4.1.1 | ||||
| * Bug修复:修复 GPT 模型 function call 调用后没有输出的问题 | ||||
| * 功能新增:允许获取 License 授权用户可以自定义版权信息 | ||||
| * 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件 | ||||
| * 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求 | ||||
| * 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用 | ||||
| * 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息 | ||||
| * 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式 | ||||
| * 功能新增:允许在管理后台设置首页显示的导航菜单 | ||||
| * Bug修复:修复注册页面先显示关闭注册组件,然后再显示注册组件 | ||||
| * 功能新增:增加 Suno 文生歌曲功能 | ||||
| * 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加 | ||||
| * 功能优化:在所有列表页面增加返回顶部按钮 | ||||
|  | ||||
| ## v4.1.0 | ||||
| * bug修复:修复移动端修改聊天标题不生效的问题 | ||||
| * Bug修复:修复用户注册不显示用户名的问题 | ||||
| * Bug修复:修复管理后台拖动排序不生效的问题 | ||||
| * 功能优化:允许用户设置自定义首页背景图片 | ||||
| * 功能新增:**支持AI解读 PDF, Word, Excel等文件** | ||||
| * 功能优化:优化聊天界面的用户上传文件的列表样式 | ||||
| * 功能优化:优化聊天页面对话样式,支持列表样式和对话样式切换 | ||||
| * 功能新增:支持微信扫码登录,未注册用户微信扫码后会自动注册并登录。移动使用微信浏览器打开可以实现无感登录。 | ||||
|  | ||||
|  | ||||
| ## v4.0.9 | ||||
| * 环境升级:升级 Golang 到 go1.22.4 | ||||
| * 功能增加:接入微信商户号支付渠道 | ||||
| * Bug修复:修复前端页面菜单把页面撑开,底部留白问题 | ||||
| * 功能优化:聊天页面自动根据内容调整输入框的高度 | ||||
| * Bug修复:修复Dalle绘图失败退回算力的问题 | ||||
| * 功能优化:邀请码注册时被邀请人也可以获得赠送的算力 | ||||
| * 功能优化:允许设置邮件验证码的抬头 | ||||
| * Bug修复:修复免费模型不会记录聊天记录的bug | ||||
| * Bug修复:修复聊天输入公式显示异常的Bug | ||||
|  | ||||
| ## v4.0.8 | ||||
| * 功能优化:升级 mathjax 公式解析插件,修复公式因为图片访问限制而无法显示的问题 | ||||
| * 功能优化:当数据库更新失败的时候记录错误日志 | ||||
| * 功能优化:聊天输入框会随着输入内容的增多自动调整高度 | ||||
| * Bug修复:修复移动端聊天页面模型切换不生效的Bug | ||||
| * 功能优化:给PC端扫码支付增加签名验证和有效期验证 | ||||
| * Bug修复:修复支付码生成API权限控制的问题 | ||||
| * Bug修复:模型算力设置为0时,不扣减用户算力,并且不记录算力消费日志 | ||||
| * 功能优化:新增随机背景配置项,可以在后台设置,首页使用 Bing 壁纸作为背景图片 | ||||
| * 功能新增:H5端支持 Dalle 绘图 | ||||
|  | ||||
| ## v4.0.7 | ||||
|  | ||||
| * 功能优化:升级quic-go,支持 Go1.21 | ||||
| * 功能优化:添加导航菜单的时候支持框入外部链接,并支持上传自定义菜单图片 | ||||
| * Bug修复:修复弹窗等于图形验证码一直验证失败的问题 | ||||
| * 功能重构:重构前端 UI 页面,增加顶部导航 | ||||
| * 功能优化:优化 Vue 非父子组件之间的通信方式 | ||||
| * 功能优化:优化 ItemList 组件,自动根据页面宽度计算 cols 数量 | ||||
|  | ||||
| ## v4.0.6 | ||||
|  | ||||
| * Bug修复:修复PC端画廊页面的瀑布流组件样式错乱问题 | ||||
| * 功能新增:给思维导图增加 ToolBar,实现思维导图的放大缩小和定位 | ||||
| * Bug修复:修复思维导图不扣费的Bug | ||||
| * Bug修复:修复管理后台角色删除失败的Bug | ||||
| * Bug修复:兼容最新版秋叶SD懒人包的 SD API,新增 scheduler 参数 | ||||
| * 功能优化:支持在管理后台配置 AI 绘图相关配置,包括 SD, MJ-PLUS, MJ-PROXY | ||||
| * Bug修复:修复注册用户提示注册人数达到上限的 Bug | ||||
| * 功能优化:将MJ,SD,Dall绘画页面的任务列表全改成瀑布流组件 | ||||
|  | ||||
| ## v4.0.5 | ||||
|  | ||||
| * 功能优化:已授权系统在后台显示授权信息 | ||||
| * 功能优化:使用思维链提示词生成思维导图,确保生成的思维导图不会出现格式错误 | ||||
| * 功能优化:优化首页登录注册页面的 UI | ||||
| * BUG修复:修复License验证的逻辑漏洞 | ||||
| * Bug修复:后台添加用户的时候密码规则限制跟前台注册保持一致 | ||||
| * 功能新增:管理后台支持切换主题,支持 light 和 dark 两种主题 | ||||
| * 功能新增:移动端新增 DALL-E 绘画功能 | ||||
| * 功能新增:新增移动端首页功能,移动端支持 light 和 dark 两种主题 | ||||
| * 功能新增:移动支持免登录预览功能 | ||||
| * Bug修复:解决在同一个浏览器开启多个对话时候对话内容会相互乱串的问题 | ||||
| * Bug修复:修复部分中转 API 模型会出现第一输出的字符被淹没的Bug | ||||
|  | ||||
| ## v4.0.4 | ||||
|  | ||||
| * Bug修复:修复统一千问第二句不回复的问题 | ||||
| * 功能优化:MJ 和 SD 任务正在执行时不更新已完成任务列表,加快页面渲染速度 | ||||
| * 功能新增:Dalle AI 绘画功能实现 | ||||
| * Bug修复:修复思维导图格式乱码问题 | ||||
| * 功能优化:支持使用 TLS 邮件协议,解决国内服务器无法使用 25 号端口发送邮件的问题 | ||||
| * 功能新增:支持从应用列表直接和某个应用对话 | ||||
| * 功能优化:优化算力日志的页面和首页的UI | ||||
| * 功能新增:支持思维导图导出 PNG 图片下载 | ||||
|  | ||||
| ## v4.0.3 | ||||
|  | ||||
| * 功能新增:允许为角色应用绑定模型,如指定某个角色只能使用某个模型 | ||||
| * Bug修复:兼容 gpt-4-turbo-2024-04-09 模型的函数调用 Bug | ||||
| * Bug修复:修复MidJourney在任务超时后出现后面的任务覆盖前面任务的问题 | ||||
| * 功能新增:支持上传图片和视觉模型 | ||||
| * 功能优化:优化聊天页面的复制代码按钮样式乱码 | ||||
| * 功能新增:增加思维导图功能,支持选择不同的对话模型来生成思维导图 | ||||
| * 功能新增:支持为角色绑定对话模型,比如绑定某个角色只能用GPT3.5或者 GPT4 | ||||
| * 功能新增:支持为模型绑定 API KEY,比如为 GPT3.5 模型绑定免费的 API KEY 给用户免费使用来引流不至于消耗你的收费 KEY。 | ||||
| * 功能新增:支持管理后台 Logo 修改 | ||||
|  | ||||
| ## 4.0.2 | ||||
|  | ||||
| * 功能新增:支持前端菜单可以配置 | ||||
| * 功能优化:在登录和注册界面标题显示软件版本号 | ||||
| * 功能优化:MJ 绘画支持 --sref 和 --cref 图片一致性参数 | ||||
| * 功能优化:使用 leveldb 解决 SD 绘图进度图片预览问题 | ||||
| * Bug修复:解决因为图片上传使用相对路径而导致融图失败的问题。 | ||||
| * 功能新增:手机端支持 Stable-Diffusion 绘画 | ||||
| * 功能新增:管理后台登录页面增加行为验证码,防止爆破 | ||||
|  | ||||
| ## v4.0.1 | ||||
|  | ||||
| * 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口,SDAPI 兼容各种 stable-diffusion | ||||
|   发行版,稳定性更强一些 | ||||
| * 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API,兼容 | ||||
|   MJ-Plus 中转 | ||||
| * 功能新增:用户算力消费日志增加统计功能,统计一段时间内用户消费的算力 | ||||
| * Bug修复:修复 iphone 手机无法通过图形验证码的Bug,使用滑动验证码替换 | ||||
| * Bug修复:修复手机端 MidJourney 绘画页面滚动条无法滚动的Bug | ||||
|  | ||||
| ## v4.0.0 | ||||
|  | ||||
| 非兼容版本,重大重构,引入算力概念,将系统中所有的能力(AI对话,MJ绘画,SD绘画,DALL绘画)全部使用算力来兑换。 | ||||
| 只要你的算力值余额不为0,你就可以进行任何操作。比如一次 GPT3.5 对话消耗1个单位算力,一次 GPT4 对话消耗10个算力。一次 MJ | ||||
| 对话消耗15个算力... | ||||
|  | ||||
| * 功能重构:重构整体系统,全部采用算力来进行结算 | ||||
| * 功能优化:SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽 | ||||
| * 功能优化:移动端聊天页面图片支持预览和放大功能 | ||||
| * 功能优化:MJ 和 SD 页面数据分页加载,解决一次性加载太多数据导致页面卡顿的问题 | ||||
| * 功能优化:**PC端不登录也可以预览功能,只有在发起操作的时候才需要登录** | ||||
| * 功能优化:控制台订单管理页面显示未支付订单,并提供订单删除功能 | ||||
| * 功能新增:支持H5支付 | ||||
| * 功能优化:支持数学公式的识别和美化输出 | ||||
| * 功能新增:新增算力消费日志功能 | ||||
| * 功能优化:整合 XXL-JOB 实现订单清理,每日算力派发,VIP 算力重置等任务 | ||||
| * 功能新增:管理后台新增7日内新增用户和新增订单统计 | ||||
|  | ||||
| ## v3.2.7 | ||||
|  | ||||
| * 功能重构:采用 Vant 重构移动页面,新增 MidJourney 功能 | ||||
| * 功能优化:优化 PC 端 MidJourney 页面布局,新增融图和换脸功能 | ||||
| * Bug修复:修复 issue [ | ||||
|   管理界面操作用户存在的两个问题](https://github.com/yangjian102621/chatgpt-plus/issues/117#issuecomment-1909201532) | ||||
| * 功能优化:在对话和聊天记录表中新增冗余字段 model,存储对话模型 | ||||
| * Bug修复:IPhone 手机验证码触摸事件坐标错位 [issue 144](https://github.com/yangjian102621/chatgpt-plus/issues/144) | ||||
| * Bug修复:重新生成按钮功能失效问题 | ||||
| * Bug修复:对话输入HTML标签不显示的问题 | ||||
| * 功能优化:gpt-4-all/gpts/midjourney-plus 支持第三方平台的 API KEY | ||||
| * 功能新增:新增删除文件功能 | ||||
| * Bug修复:解决 MJ-Plus discord 图片下载失败问题,使用第三方平台中转地址下载 | ||||
| * 功能新增:后台管理新怎对话查看和检索功能 | ||||
|  | ||||
| ## v3.2.6 | ||||
|  | ||||
| * 功能优化:恢复关闭注册系统配置项,管理员可以在后台关闭用户注册,只允许内部添加账号 | ||||
| * 功能优化:兼用旧版本微信收款消息解析 | ||||
| * 功能优化:优化订单扫码支付状态轮询功能,当关闭二维码时取消轮询,节约网络资源 | ||||
| * 功能新增:新增图片发布功能,画廊只显示用户已发布的图片 | ||||
| * 功能新增:后台新增配置微信客服二维码,可以上传自己的微信客服二维码 | ||||
| * 功能新增:新增网站公告,可以在管理后台自定义配置 | ||||
| * 功能新增:新增阿里通义千问大模型支持 | ||||
| * Bug修复:修复 MJ 放大任务失败时候 img_call 会增加的 Bug | ||||
| * 功能优化:新增虎皮椒和PayJS订单状态校验功能,增加安全性 | ||||
| * Bug修复:修复微信转账交易 ID 提取失败 Bug | ||||
| * 功能优化:给所有的 websocket 连接加上心跳,解决 "close 1006 (abnormal closure): unexpected EOF" Bug | ||||
| * 功能新增:新增短信宝短信平台发送平台集成 | ||||
|  | ||||
| ## v3.2.5 | ||||
|  | ||||
| * 功能新增:**重磅更新!!!** 新增 MidJourney-Plus API 支持,一秒配置,开箱即用,高效稳定。 | ||||
| * 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅 | ||||
|   Plus 账号了!!! | ||||
| * 功能优化:增强 markdown 图片和引用块解析。 | ||||
| * 功能新增:新增用户文件管理,目前一支持上传文件跟 GPT 进行多态对话。 | ||||
| * 功能优化:function call 兼用中转 API。 | ||||
| * Bug修复:修复部分已知的 Bug。 | ||||
|  | ||||
| ## v3.2.4.1 | ||||
|  | ||||
| * 功能新增:新增 PayJs 支付通道 | ||||
| * Bug修复:紧急修复后台添加用户失败问题 | ||||
| * Bug修复:紧急修复使用中转 API-KEY 无法绘图的问题 | ||||
| * Bug修复:允许用户关闭手机和邮箱注册通道,移除验证码依赖 | ||||
|  | ||||
| ## v3.2.4 | ||||
|  | ||||
| * 功能新增:重磅更新,支持邮箱注册 | ||||
| * 功能优化:优化函数调用授权 | ||||
| * 功能优化:给用户表新增 nickname 字段 | ||||
| * 功能优化:管理后台给聊天角色增加启用/禁用开关 | ||||
| * Bug修复:SD绘画出现重复扣减绘图次数 | ||||
| * 功能优化:优化聊天对话导出样式,适应移动端 | ||||
| * 功能新增:众筹核销可以选择兑换对话还是绘图的额度 | ||||
| * Bug修复:修复[从历史记录获取reply有并发风险 #92](https://github.com/yangjian102621/chatgpt-plus/issues/92) | ||||
| * Bug修复:修复 MidJourney 绘图任务调度Bug,为 task_id 建议唯一索引 | ||||
| * 功能重构:重构了 API KEY模块,支持为每个 API KEY 都设置不同的 API 地址,并可以单独开启是否使用代理。 | ||||
|  | ||||
| ## v3.2.3 | ||||
|  | ||||
| * 功能重构:重构函数工具模块,设计成可以后台动态管理函数。支持添加自定义函数实现 | ||||
| * 功能新增:为充值产品数据表添加 img_calls 字段,支持充值绘图次数 | ||||
| * Bug修复:修复 [MJ 机器人空指针异常的 Bug](https://github.com/yangjian102621/chatgpt-plus/issues/73) | ||||
| * Bug修复:确保相同 Prompt 的绘图任务的 Upscale 和 Variation 任务调度给相同的频道 | ||||
| * 功能新增:新增删除绘图任何和图片功能 | ||||
| * Bug修复:修复虎皮椒支付二维码重复扫码时报错问题 | ||||
| * 功能优化:自动将 AI 绘画中的中文提示词翻译成英文 | ||||
| * 功能优化:优化AI绘画的大图压缩算法,新增图片缓存 | ||||
| * 功能优化:支持为 MJ 绘图 API 增加反代功能,提高图片的加载速度,大大降低绘图任务的失败率 | ||||
| * Bug修复:修复[Azure Api 更换api-version参数后请求失败的问题](https://github.com/yangjian102621/chatgpt-plus/pull/71) | ||||
| * Bug修复:修复科大讯飞 V1.5 API 请求失败的问题 | ||||
| * Bug修复:绘图失败后,自动恢复用户的剩余绘图次数 | ||||
| * 功能新增:为移动端新增 SD 绘图功能,分享功能 | ||||
|  | ||||
| ## v3.2.2 | ||||
|  | ||||
| * 功能重构:重构 MidJourney 和 Stable-Diffusion 绘图模块,支持使用多组配置创建池子提供绘画服务 | ||||
| * 功能新增:AI绘画页面增加翻译和重写提示词功能 | ||||
| * 功能优化:OSS上传组件支持在 Bucket 下设置二级目录 | ||||
| * Bug修复:修复阿里云 OSS 访问路径错误 | ||||
| * 功能优化:在 AI 绘图页面使用 HTTP 轮询替换 Websocket | ||||
|  | ||||
| ## v3.2.1 | ||||
|  | ||||
| * 功能优化:切换角色和模型的时候自动创建新的对话 | ||||
| * Bug修复:修复文件上传失败No such file bug | ||||
| * 功能新增:MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数 | ||||
| * Bug修复:[PC端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59) | ||||
| * 功能新增:增加 arm64 架构打包脚本 | ||||
| * 功能新增:支持 dall-e3 绘图的 API 地址自定义配置 | ||||
| * 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道 | ||||
|  | ||||
| ## v3.2.0 | ||||
|  | ||||
| * 功能新增:新增邀请注册功能 | ||||
| * 功能优化:增加中间件自动对HTTP请求的参数去掉首尾空格 | ||||
| * 功能优化:增加中间件自动为大图片生成缩略图 | ||||
| * 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载 | ||||
| * 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件 | ||||
| * Bug修复:修复阿里云 OSS 域名设置不起做用的bug | ||||
| * Bug修复:修复MidJourney绘图失败后重复添加到队列的问题 | ||||
|  | ||||
| ## v3.1.9 | ||||
|  | ||||
| * 功能新增:增加讯飞星火大模型 v3.0 支持 | ||||
| * 功能新增:新增找回密码功能 | ||||
| * 功能新增:支持 Markdown 代码复制功能 | ||||
| * Bug修复: xxl-job 任务调度失败的 Bug | ||||
| * 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font | ||||
| * Bug修复:Stable-Diffusion 绘画成功之后没有扣减用户画图次数 | ||||
| * 功能优化:优化会员充值页面 ItemList 组件 | ||||
| * 功能优化:给首页 Logo 增加链接 | ||||
| * Bug修复:[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51) | ||||
| * Bug修复:聊天上下文失效问题 | ||||
| * 功能优化:关闭注册时显示联系管理员二维码 | ||||
| * 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能 | ||||
| * Bug修复:后台启用用户 VIP 不生效问题 | ||||
| * 功能优化:充值支付页面的支付说明文字可以后台配置 | ||||
| * Bug修复:ChatGLM,百度文心,科大讯飞模型输出代码不换行问题 | ||||
|  | ||||
| ## v3.1.8 | ||||
|  | ||||
| 1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道 | ||||
| 2. Bug修复:修复 MidJourney API 参数版本更新导致调用失败问题 | ||||
| 3. Bug修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题 | ||||
| 4. Bug修复:修复七牛云上传报错 expired token | ||||
| 5. Bug修复:修复高权重模型导致的对话次数为负数的漏洞 | ||||
| 6. 功能优化:将聊天报错信息定义为统一常量,方便修改 | ||||
| 7. 功能优化:优化 markdown 表格显示样式,覆写 Element-Plus 表格样式 | ||||
| 8. 功能优化:增加倒数计时组件,定期自动清理未支付的订单 | ||||
|  | ||||
| ## v3.1.7 | ||||
|  | ||||
| 1. 功能新增:支持文心4.0 AI 模型 | ||||
| 2. 功能新增:可以在管理后台为用户绑定指定的 AI 模型,如只给某个用户使用 GPT-4 模型 | ||||
| 3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如GPT4是GPT3.5的10倍 | ||||
| 4. 功能新增:新增系统配置关闭 AI 模型的函数功能 | ||||
| 5. 功能优化:优化 MidJourney 专业绘画页面图片预览样式 | ||||
|  | ||||
| ## v3.1.6 | ||||
|  | ||||
| 1. 功能新增:新增AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品 | ||||
| 2. 功能新增:新增 AI 角色应用功能页面,用户可以添加自己感兴趣的应用 | ||||
| 3. 功能优化:优化瀑布流组件的页面布局 | ||||
| 4. 功能优化:新注册用户成功之后自动登录 | ||||
| 5. 功能优化:优化更新对话标题的操作体验,绑定回车事件 | ||||
|  | ||||
| ## v3.1.5 | ||||
|  | ||||
| 1. 功能新增:新增百度文心一言大模型 API 接入支持 | ||||
| 2. 功能新增:新增科大讯飞星火大模型 API 接入支持 | ||||
| 3. 功能重构:将 chat_handler 的所有功能实现放入单独的包中 | ||||
| 4. 功能新增:新增系统配置 `enabled_function` 用于启用和关闭函数功能 | ||||
| 5. Bug修复:修复管理后台更新 API Key 失败的 Bug | ||||
| 6. Bug修复:修复新建的对话无法更新对话标题的 Bug | ||||
| 7. 功能优化:其他一些小的体验优化工作 | ||||
|  | ||||
| ## v3.1.4 | ||||
|  | ||||
| 1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio和阿里云 OSS 四种存储介质。 | ||||
| 2. 功能新增:**增加 Stable Diffusion 绘画功能页面**。 | ||||
| 3. 功能重构:将 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts) 合并到本项目,部署更加简单,无需部署两个项目了。 | ||||
| 4. Bug修复:修复[用户注册报错BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。 | ||||
| 5. Bug修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。 | ||||
| 6. 功能优化:增加阿里云短信服务配置项 `Sign` 和 `CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。 | ||||
| 7. 功能优化:添加系统配置用来设置自定义的众筹微信收款二维码。 | ||||
| 8. 功能优化:优化绘画页面的弹窗样式和页面布局。 | ||||
|  | ||||
| ## v3.1.3 | ||||
|  | ||||
| 1. 页面重构:重后 Home 页面,拆分成聊天,MJ绘画,SD 绘画,应用广场等多个功能菜单。 | ||||
| 2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。 | ||||
| 3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。 | ||||
| 4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。 | ||||
| 5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。 | ||||
| 6. Bug修复:修复 JWT token 有效期计算错误的 Bug。 | ||||
|  | ||||
| ## v3.1.2 | ||||
|  | ||||
| 1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。 | ||||
| 2. 功能新增:新增桌面版,使用 electron 套壳网页版。 | ||||
| 3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。 | ||||
| 4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。 | ||||
| 5. 功能优化:取消导出聊天页面的授权验证 | ||||
| 6. 功能优化:所有路由跳转都使用绝对路径 | ||||
|  | ||||
| ## v3.1.1 | ||||
|  | ||||
| 紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug | ||||
|  | ||||
| ## v3.1.0(大版本更新) | ||||
|  | ||||
| 1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及 | ||||
|    ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。 | ||||
| 2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。 | ||||
| 3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。 | ||||
| 4. 功能优化:更新头像自动删除旧的图片资源。 | ||||
| 5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。 | ||||
| 6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。 | ||||
| 7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug | ||||
| 8. 升级 gin, element-plus,redis 组件到最新版本。 | ||||
| 9. Bug修复:修复若干已知的的 Bug | ||||
|  | ||||
| ## v3.0.7 | ||||
|  | ||||
| 1. 聊天主界面:新增聊天引导页面,介绍产品功能 | ||||
| 2. 功能重构:拆分项目,将函数插件以及微信机器人,MidJourney 机器人等功能拆分新项目独立部署。 | ||||
| 3. 功能新增:新增 MidJourney AI 绘画支持,当识别到用户的绘画需求时,自动调用 MidJourney 绘画函数进行绘画。 | ||||
| 4. 功能新增:支持导出聊天记录为 PDF 文件。 | ||||
| 5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。 | ||||
| 6. 功能优化:支持用户设置默认的 GPT 模型 | ||||
| 7. Bug修复:修复若干已知的的 Bug | ||||
|  | ||||
| ## v3.0.6 | ||||
|  | ||||
| 1. 管理后台:新增用户名和手机号码搜索功能 | ||||
| 2. 管理后台:新增重置用户密码功能 | ||||
| 3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景 | ||||
| 4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗 | ||||
| 5. Bug修复:修复注册页面验证码不显示 Bug | ||||
| 6. Bug修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug | ||||
| 7. 功能修正:允许用户使用手机号码登录 | ||||
| 8. 功能优化:更新系统配置后同步更新服务端内存变量数据 | ||||
| 9. 功能优化:优化打包脚本,减少容器镜像大小 | ||||
|  | ||||
| ## v3.0.5 | ||||
|  | ||||
| 重磅功能更新!!! 新增函数插件支持,可以轻松地接入你的第三方插件服务,ChatGPT 自动帮您调用对应的函数完成任务。 | ||||
|  | ||||
| 1. 新增函数功能支持,全球早报,今日头条和微博热搜等插件服务,您也可以接入自己的第三方服务。 | ||||
| 2. 集成微信机器人模块,可以通过微信个人收款码来完成充值,无需接入微信支付功能也可以完成收款功能。 | ||||
| 3. 用户注册添加短信验证码功能,引入交互安全认证服务,有效防刷短信。 | ||||
| 4. 支持配置聊天上下文深度,精确统计每轮对话所消耗的总 TOKEN 数量。 | ||||
| 5. 修复已知的 Bug。 | ||||
|  | ||||
| ## v3.0.4 | ||||
|  | ||||
| 1. 调整项目目录结构,移除其他语言 API 目录 | ||||
| 2. 修复 nodejs apple M1 跨平台打包,运行报错 exec format error | ||||
| 3. 增加用户 token 消耗统计功能 | ||||
|  | ||||
| ## v3.0.3 | ||||
|  | ||||
| 1. 优化启动参数接收处理,支持环境变量传参 | ||||
| 2. 修复 PC 端聊天界面出现滚动条的 Bug | ||||
| 3. 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题 | ||||
| 4. 更改 docker 构建镜像的基础镜像,改用作者的阿里云镜像,这样打包更快一些。 | ||||
|  | ||||
| ## v3.0.2 | ||||
|  | ||||
| 1. Feat:新增移动端的聊天和用户设置功能 | ||||
| 2. Fix: 修复 markdown 换行符解析的 Bug | ||||
| 3. Feat: 新增头像上传功能 | ||||
| 4. Docs: 增加容器部署支持,支持 docker-compose 一键部署 | ||||
| 5. Fix: 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug | ||||
|  | ||||
| ## v3.0.1 | ||||
|  | ||||
| 1. 紧急修复前端 Home 组件路由被后台管理 Home 组件路由覆盖的 Bug。 | ||||
| 2. 增加 docker-compose 部署脚本 | ||||
|  | ||||
| ## v3.0.0 | ||||
|  | ||||
| 全新的重构版本!!! | ||||
| 新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。 | ||||
| 另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。 | ||||
| 前后台技术架构还是基于 `Vue3 + Element-Plus` ,但是页面风格已经全部变了,几乎所有页面样式代码都重写了,希望会你是希望的风格! | ||||
|  | ||||
| 此次重构改版主要是为了后面功能的扩展准备了。 | ||||
|  | ||||
| 新版本已经实现的功能如下: | ||||
|  | ||||
| 1. 引入用户体系,新增用户注册和登录功能。 | ||||
| 2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。 | ||||
| 3. 创建会话的时候可以选择聊天角色和模型。 | ||||
| 4. 新增聊天设置功能,用户可以导入自己的 API KEY | ||||
| 5. 保存聊天记录,支持聊天上下文。 | ||||
| 6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。 | ||||
| 7. 引入 ip2region 组件,记录用户的登录IP和地址。 | ||||
| 8. 支持会话搜索过滤。 | ||||
							
								
								
									
										11
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,11 +0,0 @@ | ||||
| # FROM 表示设置要制作的镜像基于哪个镜像,FROM指令必须是整个Dockerfile的第一个指令,如果指定的镜像不存在默认会自动从Docker Hub上下载。 | ||||
| FROM centos:7  | ||||
|  | ||||
| WORKDIR /usr/src/app | ||||
| COPY src/bin/wechatGPT-amd64-linux /usr/src/app | ||||
|  | ||||
| # 容器对外暴露的端口号,这里和配置文件保持一致就可以 | ||||
| EXPOSE 5678 | ||||
|  | ||||
| # 容器启动时执行的命令 | ||||
| CMD ["./wechatGPT-amd64-linux"] | ||||
							
								
								
									
										214
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										214
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -1,21 +1,201 @@ | ||||
| MIT License | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
| Copyright (c) 2023 RockYang | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|    1. Definitions. | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
|  | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
|  | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|   | ||||
							
								
								
									
										322
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										322
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,302 +1,76 @@ | ||||
| # ChatGPT-Plus | ||||
| # GeekAI | ||||
| > 根据[《生成式人工智能服务管理暂行办法》](https://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。 | ||||
|  | ||||
| **ChatGPT-PLUS** 是基于 OpenAI API 实现的 ChatGPT 聊天系统。主要有如下特性: | ||||
| **GeekAI** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Claude, 通义千问,Kimi,DeepSeek,Gitee AI 等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。 | ||||
|  | ||||
| * 完整的开源系统,前端应用和后台管理系统皆可开箱即用。 | ||||
| * 聊天体验跟 ChatGPT 官方版本完全一致。 | ||||
| * 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。 | ||||
| 主要特性: | ||||
|  | ||||
| **本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用,优秀项目我们将在项目首页为您展示。 | ||||
| ** | ||||
| - 完整的开源系统,前端应用和后台管理系统皆可开箱即用。 | ||||
| - 基于 Websocket 实现,完美的打字机体验。 | ||||
| - 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。 | ||||
| - 支持 OpenAI, Claude, 通义千问,Kimi,DeepSeek等多个大语言模型,**支持 Gitee AI Serverless 大模型 API**。  | ||||
| - 支持 Suno 文生音乐 | ||||
| - 支持 MidJourney / Stable Diffusion AI 绘画集成,文生图,图生图,换脸,融图。开箱即用。 | ||||
| - 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。 | ||||
| - 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。 | ||||
| - 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI | ||||
|   绘画函数插件。 | ||||
|  | ||||
| ### 🚀 更多功能请查看 [GeekAI-PLUS](https://github.com/yangjian102621/geekai-plus) | ||||
|  | ||||
| - [x] 更友好的 UI 界面 | ||||
| - [x] 支持 Dall-E 文生图功能 | ||||
| - [x] 支持文生思维导图 | ||||
| - [x] 支持为模型绑定指定的 API KEY,支持为角色绑定指定的模型等功能 | ||||
| - [x] 支持网站 Logo 版权等信息的修改 | ||||
|  | ||||
| ## 功能截图 | ||||
| 请参考 [GeekAI 项目介绍](https://docs.geekai.me/info/)。 | ||||
|  | ||||
| ### 1.PC 端聊天界面 | ||||
| ### 体验地址 | ||||
|  | ||||
|  | ||||
| > 免费体验地址:[https://chat.geekai.me](https://chat.geekai.me) <br/> | ||||
| > **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!** | ||||
|  | ||||
| ### 2. 新版聊天界面 | ||||
| ## 快速部署 | ||||
|  | ||||
|  | ||||
| 请参考文档 [**GeekAI 快速部署**](https://docs.geekai.me/install/)。 | ||||
|  | ||||
| ### 3. 用户设置 | ||||
| ## 使用须知 | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 4. 登录页面 | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 5. 管理后台 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 6. 体验地址 | ||||
|  | ||||
| > 体验地址:[https://www.chat-plus.net/chat](https://www.chat-plus.net/chat) <br/> | ||||
| > 涉及到数据隐私问题,没有提供共享账号,大家自己快速注册一个账号就可以免费体验 | ||||
|  | ||||
| ## 项目介绍 | ||||
|  | ||||
| 这一套完整的系统,包括前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供 | ||||
| ChatGPT 的服务。 | ||||
|  | ||||
| ### 项目的技术架构 | ||||
|  | ||||
| 新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。 | ||||
| 另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。 | ||||
|  | ||||
| > Gin + fx + MySQL | ||||
|  | ||||
| 3.0 版本之后会陆续添加其他语言的 API 实现,比如 PHP,Java 等。考虑到作者精力有限,api 目录已经添加了,有兴趣的同学自主去认领各自擅长的语言去实现。 | ||||
|  | ||||
| 前端的框架还是: | ||||
|  | ||||
| > Vue3 + Element-Plus | ||||
|  | ||||
| 前后台的页面风格已经全部变了,几乎所有页面样式代码都重写了。逻辑代码还是沿用之前的,毕竟功能没有太大的变化。 | ||||
|  | ||||
| 此次重构改版主要是为了后面功能的扩展准备了。 | ||||
|  | ||||
| 新版本已经实现的功能如下: | ||||
|  | ||||
| 1. 引入用户体系,新增用户注册和登录功能。 | ||||
| 2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。 | ||||
| 3. 创建会话的时候可以选择聊天角色和模型。 | ||||
| 4. 新增聊天设置功能,用户可以导入自己的 API KEY | ||||
| 5. 保存聊天记录,支持聊天上下文。 | ||||
| 7. 重构后台管理模块,更友好,扩展性更好的后台管理系统。 | ||||
| 8. 引入 ip2region 组件,记录用户的登录IP和地址。 | ||||
| 9. 支持会话搜索过滤。 | ||||
| 1. 本项目基于 Apache2.0 协议,免费开放全部源代码,可以作为个人学习使用或者商用。 | ||||
| 2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。 | ||||
|  | ||||
| ## 项目地址 | ||||
|  | ||||
| * Github 地址:https://github.com/yangjian102621/chatgpt-plus | ||||
| * 码云地址:https://gitee.com/blackfox/chatgpt-plus | ||||
| * Github 地址:https://github.com/yangjian102621/geekai | ||||
| * 码云地址:https://gitee.com/blackfox/geekai | ||||
|  | ||||
| ## 客户端下载 | ||||
|  | ||||
| 目前已经支持 Win/Linux/Mac/Android 客户端,下载地址为:https://github.com/yangjian102621/geekai/releases/tag/v3.1.2 | ||||
|  | ||||
| ## TODOLIST | ||||
|  | ||||
| * [ ] 整合 Midjourney AI 绘画 API | ||||
| * [ ] 开发移动端聊天页面 | ||||
| * [ ] 接入微信支付功能 | ||||
| * [ ] 接入语音和 TTS API,支持语音聊天 | ||||
| * [ ] 开发手机 App 客户端 | ||||
| * [ ] 支持基于知识库的 AI 问答 | ||||
| * [ ] 文生视频,文生歌曲功能 | ||||
| * [ ] 微信支付功能 | ||||
|  | ||||
| ## 安装部署 | ||||
| ## 项目文档 | ||||
|  | ||||
| 由于本项目采用的是前后端分离的开发方式,所以部署也需要前后端分开部署。我这里以 linux 系统为例,演示一下部署过程: | ||||
| 最新的部署视频教程:[https://www.bilibili.com/video/BV1Cc411t7CX/](https://www.bilibili.com/video/BV1Cc411t7CX/) | ||||
|  | ||||
| ### 1. 导入数据库 | ||||
| 详细的部署和开发文档请参考 [**GeekAI 文档**](https://docs.geekai.me)。 | ||||
|  | ||||
| ```shell | ||||
| # 下载数据库 | ||||
| wget wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v3.0.0/chatgpt_plus.sql | ||||
| # 连接数据库 | ||||
| mysql -u username -p password | ||||
| # 导入数据库 | ||||
| source chatgpt_plus.sql | ||||
| ``` | ||||
| 加微信进入微信讨论群可获取 **一键部署脚本(添加好友时请注明来自Github!!!)。** | ||||
|  | ||||
| ### 2. 修改配置文档 | ||||
|  | ||||
| 先拷贝项目中的 `api/go/config.sample.toml` 配置文档,修改代理地址和管理员密码: | ||||
|  | ||||
| ```toml | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理,如:http://127.0.0.1:7777 | ||||
| #ProxyURL = "http://127.0.0.1:7777" | ||||
| #ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了 | ||||
| MysqlDns = "mysql_user:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
|  | ||||
| [Session] | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "" | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
|   Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改 | ||||
| ``` | ||||
|  | ||||
| ### 3. 运行后端程序 | ||||
|  | ||||
| 你可以自己编译或者直接下载我打包好的后端程序运行。 | ||||
|  | ||||
| ```shell | ||||
| # 1. 下载程序,你也可以自己编译 | ||||
| wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v3.0.0/chatgpt-v3-amd64-linux | ||||
| # 2. 添加执行权限 | ||||
| chmod +x chatgpt-v3-amd64-linux | ||||
| # 3. 运行程序,如果配置文档不在当前目录,注意指定配置文档 | ||||
| ./chatgpt-v3-amd64-linux | ||||
| ``` | ||||
|  | ||||
| ### 4. 前端部署 | ||||
|  | ||||
| 前端是 Vue 项目编译好静态资源文件,同样你也可以直接下载我编译好的文件解压。 | ||||
|  | ||||
| ```shell | ||||
| # 1. 下载程序 | ||||
| wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v3.0.0/dist.tar.gz | ||||
| # 2. 解压 | ||||
| tar -xf dist.tar.gz | ||||
| ``` | ||||
|  | ||||
| ### 5. 配置 Nginx 服务 | ||||
|  | ||||
| 前端程序需要搭载 Web 服务器才可以运行,这里我们选择 Nginx,先安装: | ||||
|  | ||||
| ```shell | ||||
| sudo apt install nginx -y | ||||
| ``` | ||||
|  | ||||
| 建立 Nginx 配置文件: | ||||
|  | ||||
| ```conf | ||||
| server { | ||||
|     listen  443 ssl; | ||||
|     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/chatgpt/access.log; | ||||
|     error_log /var/log/chatgpt/error.log; | ||||
|      | ||||
|     index index.html; | ||||
|     root /var/www/chatgpt/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://localhost:5678;  | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     # 关闭静态资源的日志 | ||||
|     location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css)$ { | ||||
|         access_log off; | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| 配置好之后重启 Nginx,然后访问后台管理系统 [http://www.chatgpt.com/admin](http://www.chatgpt.com/admin), | ||||
| 输入你前面配置文档中设置的管理员用户名和密码登录。 | ||||
| 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 即可。 | ||||
|  | ||||
|  | ||||
|  | ||||
| 最后登录前端聊天页面 [http://www.chatgpt.com/admin](http://www.chatgpt.com/admin) | ||||
| 你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。 | ||||
|  | ||||
| 祝你使用愉快!!! | ||||
|  | ||||
| ## 本地开发调试 | ||||
|  | ||||
| 本地开发同样要分别运行前端和后端程序。 | ||||
|  | ||||
| ### 运行后端程序 | ||||
|  | ||||
| 1. 同样你首先要 [导入数据库](#1-导入数据库) | ||||
| 2. 然后 [修改配置文档](#2-修改配置文档) | ||||
| 3. 运行后端程序: | ||||
|  | ||||
|     ```shell | ||||
|     cd api/go  | ||||
|     # 1. 先下载依赖 | ||||
|     go mod tidy | ||||
|     # 2. 运行程序 | ||||
|     go run main.go | ||||
|     # 如果你安装了 fresh 可以使用 fresh 实现热启动 | ||||
|     fresh -c fresh.conf | ||||
|     ``` | ||||
|  | ||||
| ### 运行前端程序 | ||||
|  | ||||
| 同样先拷贝配置文档: | ||||
|  | ||||
| ```shell | ||||
| cd web | ||||
| cp .env.production .env.development | ||||
| ``` | ||||
|  | ||||
| 编辑 `.env.development` 文件,修改后端 API 的访问路径: | ||||
|  | ||||
| ```ini | ||||
| VUE_APP_API_HOST=http://localhost:5678 | ||||
| VUE_APP_WS_HOST=ws://localhost:5678 | ||||
| ``` | ||||
|  | ||||
| 配置好了之后就可以运行前端应用了: | ||||
|  | ||||
| ``` | ||||
| # 安装依赖 | ||||
| npm install | ||||
| # 运行 | ||||
| npm run dev | ||||
| ``` | ||||
|  | ||||
| * 前端页面:http://localhost:8888/chat | ||||
| * 后台管理页面:http://localhost:8888/admin | ||||
|  | ||||
| ## 项目打包 | ||||
|  | ||||
| 由于本项目是采用异构开发的方式,所项目打包分成两步:首先编译后端程序,然后再打包前端应用。 | ||||
|  | ||||
| ### 打包前端 | ||||
|  | ||||
| ```shell | ||||
| cd web | ||||
| npm run build | ||||
| ``` | ||||
|  | ||||
| ### 打包后端 | ||||
|  | ||||
| 你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。 | ||||
|  | ||||
| ```shell | ||||
| cd api/go | ||||
| # for all platforms | ||||
| make all | ||||
| # for linux only | ||||
| make linux | ||||
| ``` | ||||
|  | ||||
| 打包后的可执行文件在 `bin` 目录下。 | ||||
|  | ||||
|  | ||||
| ## 参与贡献 | ||||
|  | ||||
| 个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。 | ||||
| **尤其是新版本的开发计划比较大,包括各种语言的后端 API 实现,本人精力有限,希望借助社区的力量来完成这些 API 的开发。** | ||||
|  | ||||
| 如果有兴趣的话,也可以加微信进入微信讨论群。 | ||||
|  | ||||
|  | ||||
|  | ||||
| #### 特此声明:不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合! | ||||
| #### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合! | ||||
|  | ||||
| ### Commit 类型 | ||||
|  | ||||
| @@ -312,6 +86,6 @@ make linux | ||||
|  | ||||
| 如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										27
									
								
								Startup.sh
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								Startup.sh
									
									
									
									
									
								
							| @@ -1,27 +0,0 @@ | ||||
| # 前端 | ||||
|  | ||||
| if ! command -v node > /dev/null; then | ||||
| 	printf 'node is not installed.\n' | ||||
| 	exit 1 | ||||
| fi | ||||
| cd web | ||||
| npm install | ||||
| npm run build | ||||
| cd .. | ||||
|  | ||||
| # 后端 | ||||
| if ! command -v go > /dev/null; then | ||||
| 	printf 'go is not installed.\n' | ||||
| 	exit 1 | ||||
| fi | ||||
| cd src | ||||
| go mod tidy | ||||
| make linux | ||||
| cd .. | ||||
|  | ||||
| # Docker | ||||
| if ! command -v docker > /dev/null; then | ||||
| 	printf 'docker is not installed.\n' | ||||
| 	exit 1 | ||||
| fi | ||||
| docker compose up -d | ||||
							
								
								
									
										3
									
								
								api/go/.gitignore → api/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								api/go/.gitignore → api/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -16,3 +16,6 @@ tmp | ||||
| bin | ||||
| data | ||||
| config.toml | ||||
| static/upload  | ||||
| storage.json  | ||||
| res/certs/wechat/apiclient_key.pem | ||||
							
								
								
									
										15
									
								
								api/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								api/Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| SHELL=/usr/bin/env bash | ||||
| NAME := geekai | ||||
| all: amd64 arm64 | ||||
|  | ||||
| amd64: | ||||
| 	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-linux main.go | ||||
| .PHONY: amd64 | ||||
|  | ||||
| arm64: | ||||
| 	CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -o bin/$(NAME)-linux main.go | ||||
| .PHONY: arm64 | ||||
|  | ||||
| clean: | ||||
| 	rm -rf bin/$(NAME)-* | ||||
| .PHONY: clean | ||||
							
								
								
									
										115
									
								
								api/config.sample.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								api/config.sample.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = "" # 如 http://127.0.0.1:7777 | ||||
| MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local" | ||||
| StaticDir = "./static" # 静态资源的目录 | ||||
| StaticUrl = "/static" # 静态资源访问 URL | ||||
| TikaHost = "http://tika:9998" | ||||
|  | ||||
| [Session] | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换 | ||||
|   MaxAge = 86400 | ||||
|  | ||||
| [Redis] # redis 配置信息 | ||||
|   Host = "localhost" | ||||
|   Port = 6379 | ||||
|   Password = "" | ||||
|   DB = 0 | ||||
|  | ||||
| [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 | ||||
|   ApiURL = "https://sapi.geekai.me" | ||||
|   AppId = "" | ||||
|   Token = "" | ||||
|  | ||||
|  | ||||
| [SMS] # Sms 配置,用于发送短信 | ||||
|    Active = "Ali" # 当前启用的短信服务,默认使用阿里云 | ||||
|    [SMS.Bao] | ||||
|       Username = "" | ||||
|       Password = "" | ||||
|       Domain = "api.smsbao.com" | ||||
|       Sign = "【极客学长】" | ||||
|       CodeTemplate = "您的验证码是{code}。5分钟有效,若非本人操作,请忽略本短信。" | ||||
|    [SMS.Ali] | ||||
|       AccessKey = "" | ||||
|       AccessSecret = "" | ||||
|       Product = "Dysmsapi" | ||||
|       Domain = "dysmsapi.aliyuncs.com" | ||||
|       Sign = "" | ||||
|       CodeTempId = "" | ||||
|  | ||||
| [OSS] # OSS 配置,用于存储 MJ 绘画图片 | ||||
|    Active = "local" # 默认使用本地文件存储引擎 | ||||
|    [OSS.Local] | ||||
|      BasePath = "./static/upload" # 本地文件上传根路径 | ||||
|      BaseURL = "http://localhost:5678/static/upload" # 本地上传文件前缀 URL,线上需要把 localhost 替换成自己的实际域名或者IP | ||||
|    [OSS.Minio] | ||||
|      Endpoint = "" # 如 172.22.11.200:9000 | ||||
|      AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key | ||||
|      AccessSecret = "" | ||||
|      Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。 | ||||
|      UseSSL = false | ||||
|      Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "" | ||||
|        AccessSecret = "" | ||||
|        Bucket = "" | ||||
|        Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com | ||||
|    [OSS.AliYun] | ||||
|        Endpoint = "oss-cn-hangzhou.aliyuncs.com" | ||||
|        AccessKey = "" | ||||
|        AccessSecret = "" | ||||
|        Bucket = "chatgpt-plus" | ||||
|        SubDir = "" | ||||
|        Domain = "" | ||||
|  | ||||
| [XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动 | ||||
|   Enabled = false # 是否启用 XXL JOB 服务 | ||||
|   ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址 | ||||
|   ExecutorIp = "172.22.11.47" # 执行器 IP 地址 | ||||
|   ExecutorPort = "9999" # 执行器服务端口 | ||||
|   AccessToken = "xxl-job-api-token" # 执行器 API 通信 token | ||||
|   RegistryKey = "chatgpt-plus" # 任务注册 key | ||||
|  | ||||
|  [SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接 | ||||
|    UseTls = false | ||||
|    Host = "smtp.163.com" | ||||
|    Port = 25 | ||||
|    AppName = "极客学长" | ||||
|    From = "test@163.com" # 发件邮箱人地址 | ||||
|    Password = "" #邮箱 stmp 服务授权码 | ||||
|  | ||||
| # 支付宝商户支付 | ||||
| [AlipayConfig] | ||||
|   Enabled = false # 启用支付宝支付通道 | ||||
|   SandBox = false # 是否启用沙盒模式 | ||||
|   UserId = "2088721020750581" # 商户ID | ||||
|   AppId = "9021000131658023" # App Id | ||||
|   PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥 | ||||
|   PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书 | ||||
|   AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书 | ||||
|   RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书 | ||||
|  | ||||
| # 虎皮椒支付 | ||||
| [HuPiPayConfig] | ||||
|   Enabled = false | ||||
|   AppId = "" | ||||
|   AppSecret = "" | ||||
|   ApiURL = "https://api.xunhupay.com" | ||||
|  | ||||
| # 微信商户支付 | ||||
| [WechatPayConfig] | ||||
|   Enabled = false | ||||
|   AppId = "" # 商户应用ID | ||||
|   MchId = "" # 商户号 | ||||
|   SerialNo = "" # API 证书序列号 | ||||
|   PrivateKey = "certs/alipay/privateKey.txt" # API 证书私钥文件路径,跟支付宝一样,把私钥文件拷贝到对应的路径,证书路径要映射到容器内 | ||||
|   ApiV3Key = "" # APIV3 私钥,这个是你自己在微信支付平台设置的 | ||||
|  | ||||
| # 易支付 | ||||
| [GeekPayConfig] | ||||
|   Enabled = true | ||||
|   AppId = "" # 商户ID | ||||
|   PrivateKey = "" # 商户私钥 | ||||
|   ApiURL = "https://pay.geekai.cn" | ||||
|   Methods = ["alipay", "wxpay", "qqpay", "jdpay", "douyin", "paypal"] # 支持的支付方式 | ||||
							
								
								
									
										395
									
								
								api/core/app_server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										395
									
								
								api/core/app_server.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,395 @@ | ||||
| package core | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"image" | ||||
| 	"image/jpeg" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"runtime/debug" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"github.com/nfnt/resize" | ||||
| 	"github.com/shirou/gopsutil/host" | ||||
| 	"golang.org/x/image/webp" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type AppServer struct { | ||||
| 	Debug     bool | ||||
| 	Config    *types.AppConfig | ||||
| 	Engine    *gin.Engine | ||||
| 	SysConfig *types.SystemConfig // system config cache | ||||
| } | ||||
|  | ||||
| func NewServer(appConfig *types.AppConfig) *AppServer { | ||||
| 	gin.SetMode(gin.ReleaseMode) | ||||
| 	gin.DefaultWriter = io.Discard | ||||
| 	return &AppServer{ | ||||
| 		Debug:  false, | ||||
| 		Config: appConfig, | ||||
| 		Engine: gin.Default(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Init(debug bool, client *redis.Client) { | ||||
| 	// 允许跨域请求 API | ||||
| 	s.Engine.Use(corsMiddleware()) | ||||
| 	s.Engine.Use(staticResourceMiddleware()) | ||||
| 	s.Engine.Use(authorizeMiddleware(s, client)) | ||||
| 	s.Engine.Use(parameterHandlerMiddleware()) | ||||
| 	s.Engine.Use(errorHandler) | ||||
| 	// 添加静态资源访问 | ||||
| 	s.Engine.Static("/static", s.Config.StaticDir) | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Run(db *gorm.DB) error { | ||||
| 	// load system configs | ||||
| 	var sysConfig model.Config | ||||
| 	err := db.Where("marker", "system").First(&sysConfig).Error | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to load system config: %v", err) | ||||
| 	} | ||||
| 	err = utils.JsonDecode(sysConfig.Config, &s.SysConfig) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to decode system config: %v", err) | ||||
| 	} | ||||
| 	logger.Infof("http://%s", s.Config.Listen) | ||||
|  | ||||
| 	// 统计安装信息 | ||||
| 	go func() { | ||||
| 		info, err := host.Info() | ||||
| 		if err == nil { | ||||
| 			apiURL := fmt.Sprintf("%s/%s", s.Config.ApiConfig.ApiURL, "api/installs/push") | ||||
| 			timestamp := time.Now().Unix() | ||||
| 			product := "geekai-plus" | ||||
| 			signStr := fmt.Sprintf("%s#%s#%d", product, info.HostID, timestamp) | ||||
| 			sign := utils.Sha256(signStr) | ||||
| 			resp, err := req.C().R().SetBody(map[string]interface{}{"product": product, "device_id": info.HostID, "timestamp": timestamp, "sign": sign}).Post(apiURL) | ||||
| 			if err != nil { | ||||
| 				logger.Errorf("register install info failed: %v", err) | ||||
| 			} else { | ||||
| 				logger.Debugf("register install info success: %v", resp.String()) | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	return s.Engine.Run(s.Config.Listen) | ||||
| } | ||||
|  | ||||
| // 全局异常处理 | ||||
| func errorHandler(c *gin.Context) { | ||||
| 	defer func() { | ||||
| 		if r := recover(); r != nil { | ||||
| 			logger.Errorf("Handler Panic: %v", r) | ||||
| 			debug.PrintStack() | ||||
| 			c.JSON(http.StatusBadRequest, types.BizVo{Code: types.Failed, Message: types.ErrorMsg}) | ||||
| 			c.Abort() | ||||
| 		} | ||||
| 	}() | ||||
| 	//加载完 defer recover,继续后续接口调用 | ||||
| 	c.Next() | ||||
| } | ||||
|  | ||||
| // 跨域中间件设置 | ||||
| func corsMiddleware() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		method := c.Request.Method | ||||
| 		origin := c.Request.Header.Get("Origin") | ||||
| 		if origin != "" { | ||||
| 			// 设置允许的请求源 | ||||
| 			c.Header("Access-Control-Allow-Origin", origin) | ||||
| 			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") | ||||
| 			//允许跨域设置可以返回其他子段,可以自定义字段 | ||||
| 			c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type") | ||||
| 			// 允许浏览器(客户端)可以解析的头部 (重要) | ||||
| 			c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") | ||||
| 			//设置缓存时间 | ||||
| 			c.Header("Access-Control-Max-Age", "172800") | ||||
| 			//允许客户端传递校验信息比如 cookie (重要) | ||||
| 			c.Header("Access-Control-Allow-Credentials", "true") | ||||
| 		} | ||||
|  | ||||
| 		if method == http.MethodOptions { | ||||
| 			c.JSON(http.StatusOK, "ok!") | ||||
| 		} | ||||
|  | ||||
| 		defer func() { | ||||
| 			if err := recover(); err != nil { | ||||
| 				logger.Info("Panic info is: %v", err) | ||||
| 			} | ||||
| 		}() | ||||
|  | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 用户授权验证 | ||||
| func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		clientProtocols := c.GetHeader("Sec-WebSocket-Protocol") | ||||
| 		var tokenString string | ||||
| 		isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/") | ||||
| 		if isAdminApi { // 后台管理 API | ||||
| 			tokenString = c.GetHeader(types.AdminAuthHeader) | ||||
| 		} else if clientProtocols != "" { // Websocket 连接 | ||||
| 			// 解析子协议内容 | ||||
| 			protocols := strings.Split(clientProtocols, ",") | ||||
| 			if protocols[0] == "realtime" { | ||||
| 				tokenString = strings.TrimSpace(protocols[1][25:]) | ||||
| 			} else if protocols[0] == "token" { | ||||
| 				tokenString = strings.TrimSpace(protocols[1]) | ||||
| 			} | ||||
| 		} else { | ||||
| 			tokenString = c.GetHeader(types.UserAuthHeader) | ||||
| 		} | ||||
|  | ||||
| 		if tokenString == "" { | ||||
| 			if needLogin(c) { | ||||
| 				resp.NotAuth(c, "You should put Authorization in request headers") | ||||
| 				c.Abort() | ||||
| 				return | ||||
| 			} else { // 直接放行 | ||||
| 				c.Next() | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { | ||||
| 			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok && needLogin(c) { | ||||
| 				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | ||||
| 			} | ||||
| 			if isAdminApi { | ||||
| 				return []byte(s.Config.AdminSession.SecretKey), nil | ||||
| 			} else { | ||||
| 				return []byte(s.Config.Session.SecretKey), nil | ||||
| 			} | ||||
|  | ||||
| 		}) | ||||
|  | ||||
| 		if err != nil && needLogin(c) { | ||||
| 			resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err)) | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		claims, ok := token.Claims.(jwt.MapClaims) | ||||
| 		if !ok || !token.Valid && needLogin(c) { | ||||
| 			resp.NotAuth(c, "Token is invalid") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0) | ||||
| 		if expr > 0 && int64(expr) < time.Now().Unix() && needLogin(c) { | ||||
| 			resp.NotAuth(c, "Token is expired") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		key := fmt.Sprintf("users/%v", claims["user_id"]) | ||||
| 		if isAdminApi { | ||||
| 			key = fmt.Sprintf("admin/%v", claims["user_id"]) | ||||
| 		} | ||||
| 		if _, err := client.Get(context.Background(), key).Result(); err != nil && needLogin(c) { | ||||
| 			resp.NotAuth(c, "Token is not found in redis") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
| 		c.Set(types.LoginUserID, claims["user_id"]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func needLogin(c *gin.Context) bool { | ||||
| 	if c.Request.URL.Path == "/api/user/login" || | ||||
| 		c.Request.URL.Path == "/api/user/logout" || | ||||
| 		c.Request.URL.Path == "/api/user/resetPass" || | ||||
| 		c.Request.URL.Path == "/api/admin/login" || | ||||
| 		c.Request.URL.Path == "/api/admin/logout" || | ||||
| 		c.Request.URL.Path == "/api/admin/login/captcha" || | ||||
| 		c.Request.URL.Path == "/api/user/register" || | ||||
| 		c.Request.URL.Path == "/api/chat/history" || | ||||
| 		c.Request.URL.Path == "/api/chat/detail" || | ||||
| 		c.Request.URL.Path == "/api/chat/list" || | ||||
| 		c.Request.URL.Path == "/api/app/list" || | ||||
| 		c.Request.URL.Path == "/api/app/type/list" || | ||||
| 		c.Request.URL.Path == "/api/app/list/user" || | ||||
| 		c.Request.URL.Path == "/api/model/list" || | ||||
| 		c.Request.URL.Path == "/api/mj/imgWall" || | ||||
| 		c.Request.URL.Path == "/api/mj/notify" || | ||||
| 		c.Request.URL.Path == "/api/invite/hits" || | ||||
| 		c.Request.URL.Path == "/api/sd/imgWall" || | ||||
| 		c.Request.URL.Path == "/api/dall/imgWall" || | ||||
| 		c.Request.URL.Path == "/api/product/list" || | ||||
| 		c.Request.URL.Path == "/api/menu/list" || | ||||
| 		c.Request.URL.Path == "/api/markMap/client" || | ||||
| 		c.Request.URL.Path == "/api/payment/doPay" || | ||||
| 		c.Request.URL.Path == "/api/payment/payWays" || | ||||
| 		c.Request.URL.Path == "/api/suno/detail" || | ||||
| 		c.Request.URL.Path == "/api/suno/play" || | ||||
| 		c.Request.URL.Path == "/api/download" || | ||||
| 		strings.HasPrefix(c.Request.URL.Path, "/api/test") || | ||||
| 		strings.HasPrefix(c.Request.URL.Path, "/api/payment/notify/") || | ||||
| 		strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") || | ||||
| 		strings.HasPrefix(c.Request.URL.Path, "/api/config/") || | ||||
| 		strings.HasPrefix(c.Request.URL.Path, "/api/function/") || | ||||
| 		strings.HasPrefix(c.Request.URL.Path, "/api/sms/") || | ||||
| 		strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") || | ||||
| 		strings.HasPrefix(c.Request.URL.Path, "/static/") { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // 统一参数处理 | ||||
| func parameterHandlerMiddleware() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		// GET 参数处理 | ||||
| 		params := c.Request.URL.Query() | ||||
| 		for key, values := range params { | ||||
| 			for i, value := range values { | ||||
| 				params[key][i] = strings.TrimSpace(value) | ||||
| 			} | ||||
| 		} | ||||
| 		// update get parameters | ||||
| 		c.Request.URL.RawQuery = params.Encode() | ||||
| 		// skip file upload requests | ||||
| 		contentType := c.Request.Header.Get("Content-Type") | ||||
| 		if strings.Contains(contentType, "multipart/form-data") { | ||||
| 			c.Next() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if strings.Contains(contentType, "application/json") { | ||||
| 			// process POST JSON request body | ||||
| 			bodyBytes, err := io.ReadAll(c.Request.Body) | ||||
| 			if err != nil { | ||||
| 				c.Next() | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// 还原请求体 | ||||
| 			c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) | ||||
| 			// 将请求体解析为 JSON | ||||
| 			var jsonData map[string]interface{} | ||||
| 			if err := c.ShouldBindJSON(&jsonData); err != nil { | ||||
| 				c.Next() | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// 对 JSON 数据中的字符串值去除两端空格 | ||||
| 			trimJSONStrings(jsonData) | ||||
| 			// 更新请求体 | ||||
| 			c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData))) | ||||
| 		} | ||||
|  | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 递归对 JSON 数据中的字符串值去除两端空格 | ||||
| func trimJSONStrings(data interface{}) { | ||||
| 	switch v := data.(type) { | ||||
| 	case map[string]interface{}: | ||||
| 		for key, value := range v { | ||||
| 			switch valueType := value.(type) { | ||||
| 			case string: | ||||
| 				v[key] = strings.TrimSpace(valueType) | ||||
| 			case map[string]interface{}, []interface{}: | ||||
| 				trimJSONStrings(value) | ||||
| 			} | ||||
| 		} | ||||
| 	case []interface{}: | ||||
| 		for i, value := range v { | ||||
| 			switch valueType := value.(type) { | ||||
| 			case string: | ||||
| 				v[i] = strings.TrimSpace(valueType) | ||||
| 			case map[string]interface{}, []interface{}: | ||||
| 				trimJSONStrings(value) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 静态资源中间件 | ||||
| func staticResourceMiddleware() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
|  | ||||
| 		url := c.Request.URL.String() | ||||
| 		// 拦截生成缩略图请求 | ||||
| 		if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") { | ||||
| 			r := strings.SplitAfter(url, "imageView2") | ||||
| 			size := strings.Split(r[1], "/") | ||||
| 			if len(size) != 8 { | ||||
| 				c.String(http.StatusNotFound, "invalid thumb args") | ||||
| 				return | ||||
| 			} | ||||
| 			with := utils.IntValue(size[3], 0) | ||||
| 			height := utils.IntValue(size[5], 0) | ||||
| 			quality := utils.IntValue(size[7], 75) | ||||
|  | ||||
| 			// 打开图片文件 | ||||
| 			filePath := strings.TrimLeft(c.Request.URL.Path, "/") | ||||
| 			file, err := os.Open(filePath) | ||||
| 			if err != nil { | ||||
| 				c.String(http.StatusNotFound, "Image not found") | ||||
| 				return | ||||
| 			} | ||||
| 			defer file.Close() | ||||
|  | ||||
| 			// 解码图片 | ||||
| 			img, _, err := image.Decode(file) | ||||
| 			// for .webp image | ||||
| 			if err != nil { | ||||
| 				img, err = webp.Decode(file) | ||||
| 			} | ||||
| 			if err != nil { | ||||
| 				c.String(http.StatusInternalServerError, "Error decoding image") | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			var newImg image.Image | ||||
| 			if height == 0 || with == 0 { | ||||
| 				// 固定宽度,高度自适应 | ||||
| 				newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3) | ||||
| 			} else { | ||||
| 				// 生成缩略图 | ||||
| 				newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3) | ||||
| 			} | ||||
| 			var buffer bytes.Buffer | ||||
| 			err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality}) | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 				c.String(http.StatusInternalServerError, err.Error()) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// 设置图片缓存有效期为一年 (365天) | ||||
| 			c.Header("Cache-Control", "max-age=31536000, public") | ||||
| 			// 直接输出图像数据流 | ||||
| 			c.Data(http.StatusOK, "image/jpeg", buffer.Bytes()) | ||||
| 			c.Abort() // 中断请求 | ||||
|  | ||||
| 		} | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
| @@ -1,11 +1,17 @@ | ||||
| package core | ||||
| 
 | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/utils" | ||||
| 	"net/http" | ||||
| 	"geekai/core/types" | ||||
| 	logger2 "geekai/logger" | ||||
| 	"geekai/utils" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"github.com/BurntSushi/toml" | ||||
| @@ -15,20 +21,24 @@ var logger = logger2.GetLogger() | ||||
| 
 | ||||
| func NewDefaultConfig() *types.AppConfig { | ||||
| 	return &types.AppConfig{ | ||||
| 		Listen:   "0.0.0.0:5678", | ||||
| 		ProxyURL: "", | ||||
| 		Manager:  types.Manager{Username: "admin", Password: "admin123"}, | ||||
| 
 | ||||
| 		Listen:    "0.0.0.0:5678", | ||||
| 		ProxyURL:  "", | ||||
| 		StaticDir: "./static", | ||||
| 		StaticUrl: "http://localhost/5678/static", | ||||
| 		Redis:     types.RedisConfig{Host: "localhost", Port: 6379, Password: ""}, | ||||
| 		Session: types.Session{ | ||||
| 			SecretKey: utils.RandString(64), | ||||
| 			Name:      "CHAT_PLUS_SESSION", | ||||
| 			Domain:    "", | ||||
| 			Path:      "/", | ||||
| 			MaxAge:    86400, | ||||
| 			Secure:    true, | ||||
| 			HttpOnly:  false, | ||||
| 			SameSite:  http.SameSiteLaxMode, | ||||
| 		}, | ||||
| 		ApiConfig: types.ApiConfig{}, | ||||
| 		OSS: types.OSSConfig{ | ||||
| 			Active: "local", | ||||
| 			Local: types.LocalStorageConfig{ | ||||
| 				BaseURL:  "http://localhost/5678/static/upload", | ||||
| 				BasePath: "./static/upload", | ||||
| 			}, | ||||
| 		}, | ||||
| 		AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										120
									
								
								api/core/types/chat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								api/core/types/chat.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| // ApiRequest API 请求实体 | ||||
| type ApiRequest struct { | ||||
| 	Model               string        `json:"model,omitempty"` | ||||
| 	Temperature         float32       `json:"temperature"` | ||||
| 	MaxTokens           int           `json:"max_tokens,omitempty"` | ||||
| 	MaxCompletionTokens int           `json:"max_completion_tokens,omitempty"` // 兼容GPT O1 模型 | ||||
| 	Stream              bool          `json:"stream,omitempty"` | ||||
| 	Messages            []interface{} `json:"messages,omitempty"` | ||||
| 	Tools               []Tool        `json:"tools,omitempty"` | ||||
| 	Functions           []interface{} `json:"functions,omitempty"`       // 兼容中转平台 | ||||
| 	ResponseFormat      interface{}   `json:"response_format,omitempty"` // 响应格式 | ||||
|  | ||||
| 	ToolChoice string `json:"tool_choice,omitempty"` | ||||
|  | ||||
| 	Input      map[string]interface{} `json:"input,omitempty"`      //兼容阿里通义千问 | ||||
| 	Parameters map[string]interface{} `json:"parameters,omitempty"` //兼容阿里通义千问 | ||||
| } | ||||
|  | ||||
| type Message struct { | ||||
| 	Role    string `json:"role"` | ||||
| 	Content string `json:"content"` | ||||
| } | ||||
|  | ||||
| type ApiResponse struct { | ||||
| 	Choices []ChoiceItem `json:"choices"` | ||||
| } | ||||
|  | ||||
| // ChoiceItem API 响应实体 | ||||
| type ChoiceItem struct { | ||||
| 	Delta        Delta  `json:"delta"` | ||||
| 	FinishReason string `json:"finish_reason"` | ||||
| } | ||||
|  | ||||
| type Delta struct { | ||||
| 	Role         string      `json:"role"` | ||||
| 	Name         string      `json:"name"` | ||||
| 	Content      interface{} `json:"content"` | ||||
| 	ToolCalls    []ToolCall  `json:"tool_calls,omitempty"` | ||||
| 	FunctionCall struct { | ||||
| 		Name      string `json:"name,omitempty"` | ||||
| 		Arguments string `json:"arguments,omitempty"` | ||||
| 	} `json:"function_call,omitempty"` | ||||
| } | ||||
|  | ||||
| // ChatSession 聊天会话对象 | ||||
| type ChatSession struct { | ||||
| 	UserId   uint      `json:"user_id"` | ||||
| 	ClientIP string    `json:"client_ip"` // 客户端 IP | ||||
| 	ChatId   string    `json:"chat_id"`   // 客户端聊天会话 ID, 多会话模式专用字段 | ||||
| 	Model    ChatModel `json:"model"`     // GPT 模型 | ||||
| 	Start    int64     `json:"start"`     // 开始请求时间戳 | ||||
| 	Tools    []int     `json:"tools"`     // 工具函数列表 | ||||
| 	Stream   bool      `json:"stream"`    // 是否采用流式输出 | ||||
| } | ||||
|  | ||||
| type ChatModel struct { | ||||
| 	Id          uint    `json:"id"` | ||||
| 	Name        string  `json:"name"` | ||||
| 	Value       string  `json:"value"` | ||||
| 	Power       int     `json:"power"` | ||||
| 	MaxTokens   int     `json:"max_tokens"`  // 最大响应长度 | ||||
| 	MaxContext  int     `json:"max_context"` // 最大上下文长度 | ||||
| 	Temperature float32 `json:"temperature"` // 模型温度 | ||||
| 	KeyId       int     `json:"key_id"`      // 绑定 API KEY | ||||
| } | ||||
|  | ||||
| type ApiError struct { | ||||
| 	Error struct { | ||||
| 		Message string | ||||
| 		Type    string | ||||
| 		Param   interface{} | ||||
| 		Code    string | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const PromptMsg = "prompt" // prompt message | ||||
| const ReplyMsg = "reply"   // reply message | ||||
|  | ||||
| // PowerType 算力日志类型 | ||||
| type PowerType int | ||||
|  | ||||
| const ( | ||||
| 	PowerRecharge = PowerType(1) // 充值 | ||||
| 	PowerConsume  = PowerType(2) // 消费 | ||||
| 	PowerRefund   = PowerType(3) // 任务(SD,MJ)执行失败,退款 | ||||
| 	PowerInvite   = PowerType(4) // 邀请奖励 | ||||
| 	PowerRedeem   = PowerType(5) // 众筹 | ||||
| 	PowerGift     = PowerType(6) // 系统赠送 | ||||
| ) | ||||
|  | ||||
| func (t PowerType) String() string { | ||||
| 	switch t { | ||||
| 	case PowerRecharge: | ||||
| 		return "充值" | ||||
| 	case PowerConsume: | ||||
| 		return "消费" | ||||
| 	case PowerRefund: | ||||
| 		return "退款" | ||||
| 	case PowerRedeem: | ||||
| 		return "兑换" | ||||
|  | ||||
| 	} | ||||
| 	return "其他" | ||||
| } | ||||
|  | ||||
| type PowerMark int | ||||
|  | ||||
| const ( | ||||
| 	PowerSub = PowerMark(0) | ||||
| 	PowerAdd = PowerMark(1) | ||||
| ) | ||||
							
								
								
									
										76
									
								
								api/core/types/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								api/core/types/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"sync" | ||||
| ) | ||||
|  | ||||
| var ErrConClosed = errors.New("connection Closed") | ||||
|  | ||||
| // WsClient websocket client | ||||
| type WsClient struct { | ||||
| 	Id     string | ||||
| 	Conn   *websocket.Conn | ||||
| 	lock   sync.Mutex | ||||
| 	mt     int | ||||
| 	Closed bool | ||||
| } | ||||
|  | ||||
| func NewWsClient(conn *websocket.Conn, id string) *WsClient { | ||||
| 	return &WsClient{ | ||||
| 		Conn:   conn, | ||||
| 		Id:     id, | ||||
| 		lock:   sync.Mutex{}, | ||||
| 		mt:     2, // fixed bug for 'Invalid UTF-8 in text frame' | ||||
| 		Closed: false, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (wc *WsClient) Send(message []byte) error { | ||||
| 	wc.lock.Lock() | ||||
| 	defer wc.lock.Unlock() | ||||
|  | ||||
| 	if wc.Closed { | ||||
| 		return ErrConClosed | ||||
| 	} | ||||
|  | ||||
| 	return wc.Conn.WriteMessage(wc.mt, message) | ||||
| } | ||||
|  | ||||
| func (wc *WsClient) SendJson(value interface{}) error { | ||||
| 	wc.lock.Lock() | ||||
| 	defer wc.lock.Unlock() | ||||
|  | ||||
| 	if wc.Closed { | ||||
| 		return ErrConClosed | ||||
| 	} | ||||
| 	return wc.Conn.WriteJSON(value) | ||||
| } | ||||
|  | ||||
| func (wc *WsClient) Receive() (int, []byte, error) { | ||||
| 	if wc.Closed { | ||||
| 		return 0, nil, ErrConClosed | ||||
| 	} | ||||
|  | ||||
| 	return wc.Conn.ReadMessage() | ||||
| } | ||||
|  | ||||
| func (wc *WsClient) Close() { | ||||
| 	wc.lock.Lock() | ||||
| 	defer wc.lock.Unlock() | ||||
|  | ||||
| 	if wc.Closed { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	_ = wc.Conn.Close() | ||||
| 	wc.Closed = true | ||||
| } | ||||
							
								
								
									
										169
									
								
								api/core/types/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								api/core/types/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| type AppConfig struct { | ||||
| 	Path            string `toml:"-"` | ||||
| 	Listen          string | ||||
| 	Session         Session | ||||
| 	AdminSession    Session | ||||
| 	ProxyURL        string | ||||
| 	MysqlDns        string      // mysql 连接地址 | ||||
| 	StaticDir       string      // 静态资源目录 | ||||
| 	StaticUrl       string      // 静态资源 URL | ||||
| 	Redis           RedisConfig // redis 连接信息 | ||||
| 	ApiConfig       ApiConfig   // ChatPlus API authorization configs | ||||
| 	SMS             SMSConfig   // send mobile message config | ||||
| 	OSS             OSSConfig   // OSS config | ||||
| 	SmtpConfig      SmtpConfig  // 邮件发送配置 | ||||
| 	XXLConfig       XXLConfig | ||||
| 	AlipayConfig    AlipayConfig    // 支付宝支付渠道配置 | ||||
| 	HuPiPayConfig   HuPiPayConfig   // 虎皮椒支付配置 | ||||
| 	GeekPayConfig   GeekPayConfig   // GEEK 支付配置 | ||||
| 	WechatPayConfig WechatPayConfig // 微信支付渠道配置 | ||||
| 	TikaHost        string          // TiKa 服务器地址 | ||||
| } | ||||
|  | ||||
| type SmtpConfig struct { | ||||
| 	UseTls   bool // 是否使用 TLS 发送 | ||||
| 	Host     string | ||||
| 	Port     int | ||||
| 	AppName  string // 应用名称 | ||||
| 	From     string // 发件人邮箱地址 | ||||
| 	Password string // 发件人邮箱密码 | ||||
| } | ||||
|  | ||||
| type ApiConfig struct { | ||||
| 	ApiURL string | ||||
| 	AppId  string | ||||
| 	Token  string | ||||
| } | ||||
|  | ||||
| type AlipayConfig struct { | ||||
| 	Enabled         bool   // 是否启用该支付通道 | ||||
| 	SandBox         bool   // 是否沙盒环境 | ||||
| 	AppId           string // 应用 ID | ||||
| 	UserId          string // 支付宝用户 ID | ||||
| 	PrivateKey      string // 用户私钥文件路径 | ||||
| 	PublicKey       string // 用户公钥文件路径 | ||||
| 	AlipayPublicKey string // 支付宝公钥文件路径 | ||||
| 	RootCert        string // Root 秘钥路径 | ||||
| 	NotifyURL       string // 异步通知地址 | ||||
| 	ReturnURL       string // 同步通知地址 | ||||
| } | ||||
|  | ||||
| type WechatPayConfig struct { | ||||
| 	Enabled    bool   // 是否启用该支付通道 | ||||
| 	AppId      string // 公众号的APPID,如:wxd678efh567hg6787 | ||||
| 	MchId      string // 直连商户的商户号,由微信支付生成并下发 | ||||
| 	SerialNo   string // 商户证书的证书序列号 | ||||
| 	PrivateKey string // 用户私钥文件路径 | ||||
| 	ApiV3Key   string // API V3 秘钥 | ||||
| 	NotifyURL  string // 异步通知地址 | ||||
| } | ||||
|  | ||||
| type HuPiPayConfig struct { //虎皮椒第四方支付配置 | ||||
| 	Enabled   bool   // 是否启用该支付通道 | ||||
| 	AppId     string // App ID | ||||
| 	AppSecret string // app 密钥 | ||||
| 	ApiURL    string // 支付网关 | ||||
| 	NotifyURL string // 异步通知地址 | ||||
| 	ReturnURL string // 同步通知地址 | ||||
| } | ||||
|  | ||||
| // GeekPayConfig GEEK支付配置 | ||||
| type GeekPayConfig struct { | ||||
| 	Enabled    bool | ||||
| 	AppId      string   // 商户 ID | ||||
| 	PrivateKey string   // 私钥 | ||||
| 	ApiURL     string   // API 网关 | ||||
| 	NotifyURL  string   // 异步通知地址 | ||||
| 	ReturnURL  string   // 同步通知地址 | ||||
| 	Methods    []string // 支付方式 | ||||
| } | ||||
|  | ||||
| type XXLConfig struct { // XXL 任务调度配置 | ||||
| 	Enabled      bool | ||||
| 	ServerAddr   string | ||||
| 	ExecutorIp   string | ||||
| 	ExecutorPort string | ||||
| 	AccessToken  string | ||||
| 	RegistryKey  string | ||||
| } | ||||
|  | ||||
| type RedisConfig struct { | ||||
| 	Host     string | ||||
| 	Port     int | ||||
| 	Password string | ||||
| 	DB       int | ||||
| } | ||||
|  | ||||
| // LicenseKey 存储许可证书的 KEY | ||||
| const LicenseKey = "Geek-AI-License" | ||||
|  | ||||
| type License struct { | ||||
| 	Key       string        `json:"key"`        // 许可证书密钥 | ||||
| 	MachineId string        `json:"machine_id"` // 机器码 | ||||
| 	ExpiredAt int64         `json:"expired_at"` // 过期时间 | ||||
| 	IsActive  bool          `json:"is_active"`  // 是否激活 | ||||
| 	Configs   LicenseConfig `json:"configs"` | ||||
| } | ||||
|  | ||||
| type LicenseConfig struct { | ||||
| 	UserNum int  `json:"user_num"` // 用户数量 | ||||
| 	DeCopy  bool `json:"de_copy"`  // 去版权 | ||||
| } | ||||
|  | ||||
| func (c RedisConfig) Url() string { | ||||
| 	return fmt.Sprintf("%s:%d", c.Host, c.Port) | ||||
| } | ||||
|  | ||||
| type SystemConfig struct { | ||||
| 	Title         string `json:"title,omitempty"`           // 网站标题 | ||||
| 	Slogan        string `json:"slogan,omitempty"`          // 网站 slogan | ||||
| 	AdminTitle    string `json:"admin_title,omitempty"`     // 管理后台标题 | ||||
| 	Logo          string `json:"logo,omitempty"`            // 方形 Logo | ||||
| 	InitPower     int    `json:"init_power,omitempty"`      // 新用户注册赠送算力值 | ||||
| 	DailyPower    int    `json:"daily_power,omitempty"`     // 每日赠送算力 | ||||
| 	InvitePower   int    `json:"invite_power,omitempty"`    // 邀请新用户赠送算力值 | ||||
| 	VipMonthPower int    `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值 | ||||
|  | ||||
| 	RegisterWays    []string `json:"register_ways,omitempty"`    // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册 | ||||
| 	EnabledRegister bool     `json:"enabled_register,omitempty"` // 是否开放注册 | ||||
|  | ||||
| 	OrderPayTimeout int    `json:"order_pay_timeout,omitempty"` //订单支付超时时间 | ||||
| 	VipInfoText     string `json:"vip_info_text,omitempty"`     // 会员页面充值说明 | ||||
|  | ||||
| 	MjPower       int `json:"mj_power,omitempty"`        // MJ 绘画消耗算力 | ||||
| 	MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 | ||||
| 	SdPower       int `json:"sd_power,omitempty"`        // SD 绘画消耗算力 | ||||
| 	DallPower     int `json:"dall_power,omitempty"`      // DALL-E-3 绘图消耗算力 | ||||
| 	SunoPower     int `json:"suno_power,omitempty"`      // Suno 生成歌曲消耗算力 | ||||
| 	LumaPower     int `json:"luma_power,omitempty"`      // Luma 生成视频消耗算力 | ||||
|  | ||||
| 	WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址 | ||||
|  | ||||
| 	EnableContext bool `json:"enable_context,omitempty"` | ||||
| 	ContextDeep   int  `json:"context_deep,omitempty"` | ||||
|  | ||||
| 	SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词 | ||||
| 	MjMode      string `json:"mj_mode"`       // midjourney 默认的API模式,relax, fast, turbo | ||||
|  | ||||
| 	IndexBgURL  string `json:"index_bg_url"`  // 前端首页背景图片 | ||||
| 	IndexNavs   []int  `json:"index_navs"`    // 首页显示的导航菜单 | ||||
| 	Copyright   string `json:"copyright"`     // 版权信息 | ||||
| 	MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本 | ||||
|  | ||||
| 	EnabledVerify    bool     `json:"enabled_verify"`     // 是否启用验证码 | ||||
| 	EmailWhiteList   []string `json:"email_white_list"`   // 邮箱白名单列表 | ||||
| 	TranslateModelId int      `json:"translate_model_id"` // 用来做提示词翻译的大模型 id | ||||
| } | ||||
							
								
								
									
										27
									
								
								api/core/types/function.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								api/core/types/function.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| type ToolCall struct { | ||||
| 	Type     string `json:"type"` | ||||
| 	Function struct { | ||||
| 		Name      string `json:"name"` | ||||
| 		Arguments string `json:"arguments"` | ||||
| 	} `json:"function"` | ||||
| } | ||||
|  | ||||
| type Tool struct { | ||||
| 	Type     string   `json:"type"` | ||||
| 	Function Function `json:"function"` | ||||
| } | ||||
|  | ||||
| type Function struct { | ||||
| 	Name        string                 `json:"name"` | ||||
| 	Description string                 `json:"description"` | ||||
| 	Parameters  map[string]interface{} `json:"parameters"` | ||||
| } | ||||
| @@ -1,15 +1,22 @@ | ||||
| package types | ||||
| 
 | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| type MKey interface { | ||||
| 	string | int | ||||
| 	string | int | uint | ||||
| } | ||||
| type MValue interface { | ||||
| 	*WsClient | ChatSession | []Message | context.CancelFunc | ||||
| 	*WsClient | *ChatSession | context.CancelFunc | []interface{} | ||||
| } | ||||
| type LMap[K MKey, T MValue] struct { | ||||
| 	lock sync.RWMutex | ||||
							
								
								
									
										39
									
								
								api/core/types/order.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								api/core/types/order.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| type OrderStatus int | ||||
|  | ||||
| const ( | ||||
| 	OrderNotPaid     = OrderStatus(0) | ||||
| 	OrderScanned     = OrderStatus(1) // 已扫码 | ||||
| 	OrderPaidSuccess = OrderStatus(2) | ||||
| ) | ||||
|  | ||||
| type OrderRemark struct { | ||||
| 	Days     int     `json:"days"`  // 有效期 | ||||
| 	Power    int     `json:"power"` // 增加算力点数 | ||||
| 	Name     string  `json:"name"`  // 产品名称 | ||||
| 	Price    float64 `json:"price"` | ||||
| 	Discount float64 `json:"discount"` | ||||
| } | ||||
|  | ||||
| var PayMethods = map[string]string{ | ||||
| 	"alipay": "支付宝商号", | ||||
| 	"wechat": "微信商号", | ||||
| 	"hupi":   "虎皮椒", | ||||
| 	"geek":   "易支付", | ||||
| } | ||||
| var PayNames = map[string]string{ | ||||
| 	"alipay": "支付宝", | ||||
| 	"wxpay":  "微信支付", | ||||
| 	"qqpay":  "QQ钱包", | ||||
| 	"jdpay":  "京东支付", | ||||
| 	"douyin": "抖音支付", | ||||
| 	"paypal": "PayPal支付", | ||||
| } | ||||
							
								
								
									
										48
									
								
								api/core/types/oss.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								api/core/types/oss.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| type OSSConfig struct { | ||||
| 	Active string | ||||
| 	Local  LocalStorageConfig | ||||
| 	Minio  MiniOssConfig | ||||
| 	QiNiu  QiNiuOssConfig | ||||
| 	AliYun AliYunOssConfig | ||||
| } | ||||
| type MiniOssConfig struct { | ||||
| 	Endpoint     string | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| 	Bucket       string | ||||
| 	SubDir       string | ||||
| 	UseSSL       bool | ||||
| 	Domain       string | ||||
| } | ||||
|  | ||||
| type QiNiuOssConfig struct { | ||||
| 	Zone         string | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| 	Bucket       string | ||||
| 	SubDir       string | ||||
| 	Domain       string | ||||
| } | ||||
|  | ||||
| type AliYunOssConfig struct { | ||||
| 	Endpoint     string | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| 	Bucket       string | ||||
| 	SubDir       string | ||||
| 	Domain       string | ||||
| } | ||||
|  | ||||
| type LocalStorageConfig struct { | ||||
| 	BasePath string | ||||
| 	BaseURL  string | ||||
| } | ||||
							
								
								
									
										20
									
								
								api/core/types/session.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/core/types/session.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| const LoginUserID = "LOGIN_USER_ID" | ||||
| const LoginUserCache = "LOGIN_USER_CACHE" | ||||
|  | ||||
| const UserAuthHeader = "Authorization" | ||||
| const AdminAuthHeader = "Admin-Authorization" | ||||
|  | ||||
| // Session configs struct | ||||
| type Session struct { | ||||
| 	SecretKey string | ||||
| 	MaxAge    int | ||||
| } | ||||
							
								
								
									
										33
									
								
								api/core/types/sms.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								api/core/types/sms.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| type SMSConfig struct { | ||||
| 	Active string | ||||
| 	Ali    SmsConfigAli | ||||
| 	Bao    SmsConfigBao | ||||
| } | ||||
|  | ||||
| // SmsConfigAli 阿里云短信平台配置 | ||||
| type SmsConfigAli struct { | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| 	Product      string | ||||
| 	Domain       string | ||||
| 	Sign         string // 短信签名 | ||||
| 	CodeTempId   string // 验证码短信模板 ID | ||||
| } | ||||
|  | ||||
| // SmsConfigBao 短信宝平台配置 | ||||
| type SmsConfigBao struct { | ||||
| 	Username     string //短信宝平台注册的用户名 | ||||
| 	Password     string //短信宝平台注册的密码 | ||||
| 	Domain       string //域名 | ||||
| 	Sign         string // 短信签名 | ||||
| 	CodeTemplate string // 验证码短信模板 匹配 | ||||
| } | ||||
							
								
								
									
										134
									
								
								api/core/types/task.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								api/core/types/task.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| // TaskType 任务类别 | ||||
| type TaskType string | ||||
|  | ||||
| func (t TaskType) String() string { | ||||
| 	return string(t) | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	TaskImage     = TaskType("image") | ||||
| 	TaskBlend     = TaskType("blend") | ||||
| 	TaskSwapFace  = TaskType("swapFace") | ||||
| 	TaskUpscale   = TaskType("upscale") | ||||
| 	TaskVariation = TaskType("variation") | ||||
| ) | ||||
|  | ||||
| // MjTask MidJourney 任务 | ||||
| type MjTask struct { | ||||
| 	Id               uint     `json:"id"`      // 任务ID | ||||
| 	TaskId           string   `json:"task_id"` // 中转任务ID | ||||
| 	ClientId         string   `json:"client_id"` | ||||
| 	ImgArr           []string `json:"img_arr"` | ||||
| 	Type             TaskType `json:"type"` | ||||
| 	UserId           int      `json:"user_id"` | ||||
| 	Prompt           string   `json:"prompt,omitempty"` | ||||
| 	NegPrompt        string   `json:"neg_prompt,omitempty"` | ||||
| 	Params           string   `json:"full_prompt"` | ||||
| 	Index            int      `json:"index,omitempty"` | ||||
| 	MessageId        string   `json:"message_id,omitempty"` | ||||
| 	MessageHash      string   `json:"message_hash,omitempty"` | ||||
| 	ChannelId        string   `json:"channel_id"`         // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道 | ||||
| 	Mode             string   `json:"mode"`               // 绘画模式,relax, fast, turbo | ||||
| 	TranslateModelId int      `json:"translate_model_id"` // 提示词翻译模型ID | ||||
| } | ||||
|  | ||||
| type SdTask struct { | ||||
| 	Id               int          `json:"id"` // job 数据库ID | ||||
| 	Type             TaskType     `json:"type"` | ||||
| 	ClientId         string       `json:"client_id"` | ||||
| 	UserId           int          `json:"user_id"` | ||||
| 	Params           SdTaskParams `json:"params"` | ||||
| 	RetryCount       int          `json:"retry_count"` | ||||
| 	TranslateModelId int          `json:"translate_model_id"` // 提示词翻译模型ID | ||||
| } | ||||
|  | ||||
| type SdTaskParams struct { | ||||
| 	ClientId     string  `json:"client_id"` // 客户端ID | ||||
| 	TaskId       string  `json:"task_id"` | ||||
| 	Prompt       string  `json:"prompt"`     // 提示词 | ||||
| 	NegPrompt    string  `json:"neg_prompt"` // 反向提示词 | ||||
| 	Steps        int     `json:"steps"`      // 迭代步数,默认20 | ||||
| 	Sampler      string  `json:"sampler"`    // 采样器 | ||||
| 	Scheduler    string  `json:"scheduler"`  // 采样调度 | ||||
| 	FaceFix      bool    `json:"face_fix"`   // 面部修复 | ||||
| 	CfgScale     float32 `json:"cfg_scale"`  //引导系数,默认 7 | ||||
| 	Seed         int64   `json:"seed"`       // 随机数种子 | ||||
| 	Height       int     `json:"height"` | ||||
| 	Width        int     `json:"width"` | ||||
| 	HdFix        bool    `json:"hd_fix"`         // 启用高清修复 | ||||
| 	HdRedrawRate float32 `json:"hd_redraw_rate"` // 高清修复重绘幅度 | ||||
| 	HdScale      int     `json:"hd_scale"`       // 放大倍数 | ||||
| 	HdScaleAlg   string  `json:"hd_scale_alg"`   // 放大算法 | ||||
| 	HdSteps      int     `json:"hd_steps"`       // 高清修复迭代步数 | ||||
| } | ||||
|  | ||||
| // DallTask DALL-E task | ||||
| type DallTask struct { | ||||
| 	ClientId string `json:"client_id"` | ||||
| 	Id       uint   `json:"id"` | ||||
| 	UserId   uint   `json:"user_id"` | ||||
| 	Prompt   string `json:"prompt"` | ||||
| 	N        int    `json:"n"` | ||||
| 	Quality  string `json:"quality"` | ||||
| 	Size     string `json:"size"` | ||||
| 	Style    string `json:"style"` | ||||
|  | ||||
| 	Power            int `json:"power"` | ||||
| 	TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID | ||||
| } | ||||
|  | ||||
| type SunoTask struct { | ||||
| 	ClientId     string `json:"client_id"` | ||||
| 	Id           uint   `json:"id"` | ||||
| 	Channel      string `json:"channel"` | ||||
| 	UserId       int    `json:"user_id"` | ||||
| 	Type         int    `json:"type"` | ||||
| 	Title        string `json:"title"` | ||||
| 	RefTaskId    string `json:"ref_task_id,omitempty"` | ||||
| 	RefSongId    string `json:"ref_song_id,omitempty"` | ||||
| 	Prompt       string `json:"prompt"` // 提示词/歌词 | ||||
| 	Tags         string `json:"tags"` | ||||
| 	Model        string `json:"model"` | ||||
| 	Instrumental bool   `json:"instrumental"`          // 是否纯音乐 | ||||
| 	ExtendSecs   int    `json:"extend_secs,omitempty"` // 延长秒杀 | ||||
| 	SongId       string `json:"song_id,omitempty"`     // 合并歌曲ID | ||||
| 	AudioURL     string `json:"audio_url"`             // 用户上传音频地址 | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	VideoLuma   = "luma" | ||||
| 	VideoRunway = "runway" | ||||
| 	VideoCog    = "cog" | ||||
| ) | ||||
|  | ||||
| type VideoTask struct { | ||||
| 	ClientId         string      `json:"client_id"` | ||||
| 	Id               uint        `json:"id"` | ||||
| 	Channel          string      `json:"channel"` | ||||
| 	UserId           int         `json:"user_id"` | ||||
| 	Type             string      `json:"type"` | ||||
| 	TaskId           string      `json:"task_id"` | ||||
| 	Prompt           string      `json:"prompt"` // 提示词 | ||||
| 	Params           VideoParams `json:"params"` | ||||
| 	TranslateModelId int         `json:"translate_model_id"` // 提示词翻译模型ID | ||||
| } | ||||
|  | ||||
| type VideoParams struct { | ||||
| 	PromptOptimize bool   `json:"prompt_optimize"` // 是否优化提示词 | ||||
| 	Loop           bool   `json:"loop"`            // 是否循环参考图 | ||||
| 	StartImgURL    string `json:"start_img_url"`   // 第一帧参考图地址 | ||||
| 	EndImgURL      string `json:"end_img_url"`     // 最后一帧参考图地址 | ||||
| 	Model          string `json:"model"`           // 使用哪个模型生成视频 | ||||
| 	Radio          string `json:"radio"`           // 视频尺寸 | ||||
| 	Style          string `json:"style"`           // 风格 | ||||
| 	Duration       int    `json:"duration"`        // 视频时长(秒) | ||||
| } | ||||
							
								
								
									
										72
									
								
								api/core/types/web.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								api/core/types/web.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| package types | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| // BizVo 业务返回 VO | ||||
| type BizVo struct { | ||||
| 	Code     BizCode     `json:"code"` | ||||
| 	Page     int         `json:"page,omitempty"` | ||||
| 	PageSize int         `json:"page_size,omitempty"` | ||||
| 	Total    int         `json:"total,omitempty"` | ||||
| 	Message  string      `json:"message,omitempty"` | ||||
| 	Data     interface{} `json:"data,omitempty"` | ||||
| } | ||||
|  | ||||
| // ReplyMessage 对话回复消息结构 | ||||
| type ReplyMessage struct { | ||||
| 	Channel  WsChannel   `json:"channel"`  // 消息频道,目前只有 chat | ||||
| 	ClientId string      `json:"clientId"` // 客户端ID | ||||
| 	Type     WsMsgType   `json:"type"`     // 消息类别 | ||||
| 	Body     interface{} `json:"body"` | ||||
| } | ||||
|  | ||||
| type WsMsgType string | ||||
| type WsChannel string | ||||
|  | ||||
| const ( | ||||
| 	MsgTypeText = WsMsgType("text") // 输出内容 | ||||
| 	MsgTypeEnd  = WsMsgType("end") | ||||
| 	MsgTypeErr  = WsMsgType("error") | ||||
| 	MsgTypePing = WsMsgType("ping") // 心跳消息 | ||||
|  | ||||
| 	ChPing = WsChannel("ping") | ||||
| 	ChChat = WsChannel("chat") | ||||
| 	ChMj   = WsChannel("mj") | ||||
| 	ChSd   = WsChannel("sd") | ||||
| 	ChDall = WsChannel("dall") | ||||
| 	ChSuno = WsChannel("suno") | ||||
| 	ChLuma = WsChannel("luma") | ||||
| ) | ||||
|  | ||||
| // InputMessage 对话输入消息结构 | ||||
| type InputMessage struct { | ||||
| 	Channel WsChannel   `json:"channel"` // 消息频道 | ||||
| 	Type    WsMsgType   `json:"type"`    // 消息类别 | ||||
| 	Body    interface{} `json:"body"` | ||||
| } | ||||
|  | ||||
| type ChatMessage struct { | ||||
| 	Tools   []int  `json:"tools,omitempty"`  // 允许调用工具列表 | ||||
| 	Stream  bool   `json:"stream,omitempty"` // 是否采用流式输出 | ||||
| 	RoleId  int    `json:"role_id"` | ||||
| 	ModelId int    `json:"model_id"` | ||||
| 	ChatId  string `json:"chat_id"` | ||||
| 	Content string `json:"content"` | ||||
| } | ||||
|  | ||||
| type BizCode int | ||||
|  | ||||
| const ( | ||||
| 	Success       = BizCode(0) | ||||
| 	Failed        = BizCode(1) | ||||
| 	NotAuthorized = BizCode(401) // 未授权 | ||||
|  | ||||
| 	OkMsg       = "Success" | ||||
| 	ErrorMsg    = "系统开小差了" | ||||
| 	InvalidArgs = "非法参数或参数解析失败" | ||||
| ) | ||||
							
								
								
									
										120
									
								
								api/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								api/go.mod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| module geekai | ||||
|  | ||||
| go 1.21 | ||||
|  | ||||
| toolchain go1.22.4 | ||||
|  | ||||
| require ( | ||||
| 	github.com/BurntSushi/toml v1.1.0 | ||||
| 	github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 | ||||
| 	github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible | ||||
| 	github.com/gin-gonic/gin v1.9.1 | ||||
| 	github.com/go-redis/redis/v8 v8.11.5 | ||||
| 	github.com/golang-jwt/jwt/v5 v5.0.0 | ||||
| 	github.com/gorilla/websocket v1.5.0 | ||||
| 	github.com/imroc/req/v3 v3.37.2 | ||||
| 	github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 | ||||
| 	github.com/minio/minio-go/v7 v7.0.62 | ||||
| 	github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 | ||||
| 	github.com/qiniu/go-sdk/v7 v7.17.1 | ||||
| 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e | ||||
| 	go.uber.org/zap v1.23.0 | ||||
| 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 | ||||
| 	gorm.io/driver/mysql v1.4.7 | ||||
| ) | ||||
|  | ||||
| require github.com/xxl-job/xxl-job-executor-go v1.2.0 | ||||
|  | ||||
| require ( | ||||
| 	github.com/go-pay/gopay v1.5.101 | ||||
| 	github.com/google/go-tika v0.3.1 | ||||
| 	github.com/microcosm-cc/bluemonday v1.0.26 | ||||
| 	github.com/shopspring/decimal v1.3.1 | ||||
| 	github.com/syndtr/goleveldb v1.0.0 | ||||
| 	golang.org/x/image v0.15.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/aymerick/douceur v0.2.0 // indirect | ||||
| 	github.com/go-ole/go-ole v1.2.6 // indirect | ||||
| 	github.com/go-pay/crypto v0.0.1 // indirect | ||||
| 	github.com/go-pay/errgroup v0.0.2 // indirect | ||||
| 	github.com/go-pay/util v0.0.2 // indirect | ||||
| 	github.com/go-pay/xlog v0.0.2 // indirect | ||||
| 	github.com/go-pay/xtime v0.0.2 // indirect | ||||
| 	github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect | ||||
| 	github.com/gorilla/css v1.0.0 // indirect | ||||
| 	github.com/shirou/gopsutil v3.21.11+incompatible // indirect | ||||
| 	github.com/tklauser/go-sysconf v0.3.13 // indirect | ||||
| 	github.com/tklauser/numcpus v0.7.0 // indirect | ||||
| 	github.com/yusufpapurcu/wmi v1.2.4 // indirect | ||||
| 	go.uber.org/mock v0.4.0 // indirect | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/andybalholm/brotli v1.0.4 // indirect | ||||
| 	github.com/bytedance/sonic v1.9.1 // indirect | ||||
| 	github.com/cespare/xxhash/v2 v2.2.0 // indirect | ||||
| 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect | ||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||
| 	github.com/dlclark/regexp2 v1.8.1 // indirect | ||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.2 // indirect | ||||
| 	github.com/gaukas/godicttls v0.0.3 // indirect | ||||
| 	github.com/go-basic/ipv4 v1.0.0 // indirect | ||||
| 	github.com/go-sql-driver/mysql v1.7.0 // indirect | ||||
| 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect | ||||
| 	github.com/goccy/go-json v0.10.2 // indirect | ||||
| 	github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect | ||||
| 	github.com/google/uuid v1.3.0 // indirect | ||||
| 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||
| 	github.com/hashicorp/go-multierror v1.1.1 // indirect | ||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||
| 	github.com/jinzhu/now v1.1.5 // indirect | ||||
| 	github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect | ||||
| 	github.com/klauspost/compress v1.16.7 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.5 // indirect | ||||
| 	github.com/minio/md5-simd v1.1.2 // indirect | ||||
| 	github.com/minio/sha256-simd v1.0.1 // indirect | ||||
| 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 | ||||
| 	github.com/onsi/ginkgo/v2 v2.10.0 // indirect | ||||
| 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect | ||||
| 	github.com/quic-go/qpack v0.4.0 // indirect | ||||
| 	github.com/quic-go/quic-go v0.45.0 // indirect | ||||
| 	github.com/refraction-networking/utls v1.3.2 // indirect | ||||
| 	github.com/rs/xid v1.5.0 // indirect | ||||
| 	github.com/sirupsen/logrus v1.9.3 // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	go.uber.org/dig v1.16.1 // indirect | ||||
| 	golang.org/x/arch v0.3.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect | ||||
| 	golang.org/x/mod v0.17.0 // indirect | ||||
| 	golang.org/x/net v0.25.0 // indirect | ||||
| 	golang.org/x/sync v0.7.0 // indirect | ||||
| 	golang.org/x/text v0.15.0 // indirect | ||||
| 	golang.org/x/time v0.5.0 // indirect | ||||
| 	golang.org/x/tools v0.21.0 // indirect | ||||
| 	google.golang.org/protobuf v1.33.0 // indirect | ||||
| 	gopkg.in/ini.v1 v1.67.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.14.0 // indirect | ||||
| 	github.com/json-iterator/go v1.1.12 // indirect | ||||
| 	github.com/leodido/go-urn v1.2.4 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.19 // indirect | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.11 // indirect | ||||
| 	go.uber.org/atomic v1.9.0 // indirect | ||||
| 	go.uber.org/fx v1.19.3 | ||||
| 	go.uber.org/multierr v1.6.0 // indirect | ||||
| 	golang.org/x/crypto v0.23.0 | ||||
| 	golang.org/x/sys v0.20.0 // indirect | ||||
| 	gorm.io/gorm v1.25.1 | ||||
| ) | ||||
							
								
								
									
										354
									
								
								api/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										354
									
								
								api/go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,354 @@ | ||||
| github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= | ||||
| github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | ||||
| github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4= | ||||
| github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs= | ||||
| github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k= | ||||
| github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= | ||||
| github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= | ||||
| github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= | ||||
| github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= | ||||
| github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= | ||||
| github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= | ||||
| github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= | ||||
| github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | ||||
| github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= | ||||
| github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | ||||
| github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= | ||||
| github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | ||||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | ||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||||
| github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= | ||||
| github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | ||||
| github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||||
| github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||||
| github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||||
| github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= | ||||
| github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= | ||||
| github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= | ||||
| github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= | ||||
| github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= | ||||
| github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= | ||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||
| github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= | ||||
| github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= | ||||
| github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs= | ||||
| github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg= | ||||
| github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= | ||||
| github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||||
| github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= | ||||
| github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= | ||||
| github.com/go-pay/crypto v0.0.1 h1:B6InT8CLfSLc6nGRVx9VMJRBBazFMjr293+jl0lLXUY= | ||||
| github.com/go-pay/crypto v0.0.1/go.mod h1:41oEIvHMKbNcYlWUlRWtsnC6+ASgh7u29z0gJXe5bes= | ||||
| github.com/go-pay/errgroup v0.0.2 h1:5mZMdm0TDClDm2S3G0/sm0f8AuQRtz0dOrTHDR9R8Cc= | ||||
| github.com/go-pay/errgroup v0.0.2/go.mod h1:0+4b8mvFMS71MIzsaC+gVvB4x37I93lRb2dqrwuU8x8= | ||||
| github.com/go-pay/gopay v1.5.101 h1:rVb+sfv6hiQtknAlZnTTLvU27NvFJ4p0yglN/vPpGXI= | ||||
| github.com/go-pay/gopay v1.5.101/go.mod h1:AW4Yj8jDZX9BM1/GTLTY1Gy5SHjiq8kQvG5sBTN2sxI= | ||||
| github.com/go-pay/util v0.0.2 h1:goJ4f6kNY5zzdtg1Cj8oWC+Cw7bfg/qq2rJangMAb9U= | ||||
| github.com/go-pay/util v0.0.2/go.mod h1:qM8VbyF1n7YAPZBSJONSPMPsPedhUTktewUAdf1AjPg= | ||||
| github.com/go-pay/xlog v0.0.2 h1:kUg5X8/5VZAPDg1J5eGjA3MG0/H5kK6Ew0dW/Bycsws= | ||||
| github.com/go-pay/xlog v0.0.2/go.mod h1:DbjMADPK4+Sjxj28ekK9goqn4zmyY4hql/zRiab+S9E= | ||||
| github.com/go-pay/xtime v0.0.2 h1:7YR4/iuELsEHpJ6LUO0SVK80hQxDO9MLCfuVYIiTCRM= | ||||
| github.com/go-pay/xtime v0.0.2/go.mod h1:W1yRbJaSt4CSBcdAtLBQ8xajiN/Pl5hquGczUcUE9xE= | ||||
| github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||
| github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= | ||||
| github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= | ||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||
| github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= | ||||
| github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= | ||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||
| github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk= | ||||
| github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= | ||||
| github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||
| github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= | ||||
| github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= | ||||
| github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= | ||||
| github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||
| github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= | ||||
| github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= | ||||
| github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | ||||
| github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | ||||
| github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= | ||||
| github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= | ||||
| github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= | ||||
| github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= | ||||
| github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= | ||||
| github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||
| github.com/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-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA= | ||||
| github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= | ||||
| github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= | ||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= | ||||
| github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= | ||||
| github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||
| github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= | ||||
| github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | ||||
| github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I= | ||||
| github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI= | ||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||
| github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= | ||||
| github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= | ||||
| github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||
| github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= | ||||
| github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= | ||||
| github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= | ||||
| github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||
| github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | ||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||
| github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= | ||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= | ||||
| github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= | ||||
| github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= | ||||
| github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs= | ||||
| github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs= | ||||
| github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= | ||||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= | ||||
| github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= | ||||
| github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= | ||||
| github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= | ||||
| github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc= | ||||
| github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= | ||||
| github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= | ||||
| github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||
| github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= | ||||
| github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= | ||||
| github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= | ||||
| github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= | ||||
| github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | ||||
| github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= | ||||
| github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs= | ||||
| github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE= | ||||
| github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||
| github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= | ||||
| github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= | ||||
| github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= | ||||
| github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= | ||||
| github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= | ||||
| github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= | ||||
| github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= | ||||
| github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0= | ||||
| github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w= | ||||
| github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= | ||||
| github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= | ||||
| github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= | ||||
| github.com/quic-go/quic-go v0.45.0 h1:OHmkQGM37luZITyTSu6ff03HP/2IrwDX1ZFiNEhSFUE= | ||||
| github.com/quic-go/quic-go v0.45.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= | ||||
| github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= | ||||
| github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= | ||||
| github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | ||||
| github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | ||||
| github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= | ||||
| github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= | ||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||
| github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= | ||||
| github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= | ||||
| github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= | ||||
| github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= | ||||
| github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | ||||
| github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | ||||
| github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= | ||||
| github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= | ||||
| github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||
| github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= | ||||
| github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= | ||||
| github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= | ||||
| github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= | ||||
| github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= | ||||
| github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||
| github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= | ||||
| github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= | ||||
| github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= | ||||
| github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= | ||||
| github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= | ||||
| github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||
| github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ= | ||||
| github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo= | ||||
| github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||
| github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= | ||||
| github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= | ||||
| go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= | ||||
| go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= | ||||
| go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= | ||||
| go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA= | ||||
| go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM= | ||||
| go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= | ||||
| go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= | ||||
| go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= | ||||
| go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= | ||||
| go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= | ||||
| go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= | ||||
| go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= | ||||
| go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= | ||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= | ||||
| golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= | ||||
| golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||||
| golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= | ||||
| golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= | ||||
| golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= | ||||
| golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= | ||||
| golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= | ||||
| golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= | ||||
| golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= | ||||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||
| golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||
| golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= | ||||
| golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||
| golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= | ||||
| golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||
| golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||||
| golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= | ||||
| golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= | ||||
| golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= | ||||
| golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= | ||||
| golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||
| golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= | ||||
| golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||||
| golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= | ||||
| golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= | ||||
| golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||
| golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | ||||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||
| golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= | ||||
| golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||
| golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= | ||||
| golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||
| golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | ||||
| golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= | ||||
| golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= | ||||
| google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||
| gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= | ||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||
| gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | ||||
| gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= | ||||
| gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= | ||||
| gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= | ||||
| gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= | ||||
| gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= | ||||
| rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | ||||
| @@ -1,20 +0,0 @@ | ||||
| SHELL=/usr/bin/env bash | ||||
| NAME := chatgpt-v3 | ||||
| all: window linux darwin | ||||
|  | ||||
|  | ||||
| window: | ||||
| 	CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/$(NAME)-amd64.exe main.go | ||||
| .PHONY: window | ||||
|  | ||||
| linux: | ||||
| 	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-amd64-linux main.go | ||||
| .PHONY: linux | ||||
|  | ||||
| darwin: | ||||
| 	CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go | ||||
| .PHONY: darwin | ||||
|  | ||||
| clean: | ||||
| 	rm -rf bin/$(NAME)-* | ||||
| .PHONY: clean | ||||
| @@ -1,17 +0,0 @@ | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = ["YOUR_PROXY_URL"] | ||||
| MysqlDns = "mysql_user:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
|  | ||||
| [Session] | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "" | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
|   Password = "admin123" | ||||
| @@ -1,169 +0,0 @@ | ||||
| package core | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"context" | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-contrib/sessions/cookie" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"runtime/debug" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type AppServer struct { | ||||
| 	AppConfig    *types.AppConfig | ||||
| 	Engine       *gin.Engine | ||||
| 	ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message | ||||
| 	ChatConfig   *types.ChatConfig                    // 聊天配置 | ||||
|  | ||||
| 	// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次 | ||||
| 	// 防止第三方直接连接 socket 调用 OpenAI API | ||||
| 	ChatSession   *types.LMap[string, types.ChatSession]  //map[sessionId]UserId | ||||
| 	ChatClients   *types.LMap[string, *types.WsClient]    // Websocket 连接集合 | ||||
| 	ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function | ||||
| } | ||||
|  | ||||
| func NewServer(appConfig *types.AppConfig) *AppServer { | ||||
| 	gin.SetMode(gin.ReleaseMode) | ||||
| 	gin.DefaultWriter = io.Discard | ||||
| 	return &AppServer{ | ||||
| 		AppConfig:     appConfig, | ||||
| 		Engine:        gin.Default(), | ||||
| 		ChatContexts:  types.NewLMap[string, []types.Message](), | ||||
| 		ChatSession:   types.NewLMap[string, types.ChatSession](), | ||||
| 		ChatClients:   types.NewLMap[string, *types.WsClient](), | ||||
| 		ReqCancelFunc: types.NewLMap[string, context.CancelFunc](), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Init(debug bool) { | ||||
| 	if debug { // 调试模式允许跨域请求 API | ||||
| 		logger.Info("Enabled debug mode") | ||||
| 		s.Engine.Use(corsMiddleware()) | ||||
| 	} | ||||
| 	s.Engine.Use(sessionMiddleware(s.AppConfig)) | ||||
| 	s.Engine.Use(authorizeMiddleware(s)) | ||||
| 	s.Engine.Use(errorHandler) | ||||
| 	//gob.Register(model.User{}) | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Run(db *gorm.DB) error { | ||||
| 	// load chat config from database | ||||
| 	var config model.Config | ||||
| 	res := db.Where("marker", "chat").First(&config) | ||||
| 	if res.Error != nil { | ||||
| 		return res.Error | ||||
| 	} | ||||
| 	err := utils.JsonDecode(config.Config, &s.ChatConfig) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	logger.Infof("http://%s", s.AppConfig.Listen) | ||||
| 	return s.Engine.Run(s.AppConfig.Listen) | ||||
| } | ||||
|  | ||||
| // 全局异常处理 | ||||
| func errorHandler(c *gin.Context) { | ||||
| 	defer func() { | ||||
| 		if r := recover(); r != nil { | ||||
| 			logger.Error("panic: %v\n", r) | ||||
| 			debug.PrintStack() | ||||
| 			c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg}) | ||||
| 			c.Abort() | ||||
| 		} | ||||
| 	}() | ||||
| 	//加载完 defer recover,继续后续接口调用 | ||||
| 	c.Next() | ||||
| } | ||||
|  | ||||
| // 会话处理 | ||||
| func sessionMiddleware(config *types.AppConfig) gin.HandlerFunc { | ||||
| 	// encrypt the cookie | ||||
| 	store := cookie.NewStore([]byte(config.Session.SecretKey)) | ||||
| 	store.Options(sessions.Options{ | ||||
| 		Path:     config.Session.Path, | ||||
| 		Domain:   config.Session.Domain, | ||||
| 		MaxAge:   config.Session.MaxAge, | ||||
| 		Secure:   config.Session.Secure, | ||||
| 		HttpOnly: config.Session.HttpOnly, | ||||
| 		SameSite: config.Session.SameSite, | ||||
| 	}) | ||||
| 	return sessions.Sessions(config.Session.Name, store) | ||||
| } | ||||
|  | ||||
| // 跨域中间件设置 | ||||
| func corsMiddleware() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		method := c.Request.Method | ||||
| 		origin := c.Request.Header.Get("Origin") | ||||
| 		if origin != "" { | ||||
| 			// 设置允许的请求源 | ||||
| 			c.Header("Access-Control-Allow-Origin", origin) | ||||
| 			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") | ||||
| 			//允许跨域设置可以返回其他子段,可以自定义字段 | ||||
| 			c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, ChatGPT-TOKEN, ADMIN-SESSION-TOKEN") | ||||
| 			// 允许浏览器(客户端)可以解析的头部 (重要) | ||||
| 			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") | ||||
| 			//设置缓存时间 | ||||
| 			c.Header("Access-Control-Max-Age", "172800") | ||||
| 			//允许客户端传递校验信息比如 cookie (重要) | ||||
| 			c.Header("Access-Control-Allow-Credentials", "true") | ||||
| 		} | ||||
|  | ||||
| 		if method == http.MethodOptions { | ||||
| 			c.JSON(http.StatusOK, "ok!") | ||||
| 		} | ||||
|  | ||||
| 		defer func() { | ||||
| 			if err := recover(); err != nil { | ||||
| 				logger.Info("Panic info is: %v", err) | ||||
| 			} | ||||
| 		}() | ||||
|  | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 用户授权验证 | ||||
| func authorizeMiddleware(s *AppServer) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		if c.Request.URL.Path == "/api/user/login" || | ||||
| 			c.Request.URL.Path == "/api/admin/login" || | ||||
| 			c.Request.URL.Path == "/api/user/register" || | ||||
| 			c.Request.URL.Path == "/api/admin/config/get" { | ||||
| 			c.Next() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// WebSocket 连接请求验证 | ||||
| 		if c.Request.URL.Path == "/api/chat" { | ||||
| 			sessionId := c.Query("sessionId") | ||||
| 			session := s.ChatSession.Get(sessionId) | ||||
| 			if session.ClientIP == c.ClientIP() { | ||||
| 				c.Next() | ||||
| 			} else { | ||||
| 				c.Abort() | ||||
| 			} | ||||
| 			return | ||||
| 		} | ||||
| 		session := sessions.Default(c) | ||||
| 		var value interface{} | ||||
| 		if strings.Contains(c.Request.URL.Path, "/api/admin/") { | ||||
| 			value = session.Get(types.SessionAdmin) | ||||
| 		} else { | ||||
| 			value = session.Get(types.SessionUser) | ||||
| 		} | ||||
| 		if value != nil { | ||||
| 			c.Next() | ||||
| 		} else { | ||||
| 			resp.NotAuth(c) | ||||
| 			c.Abort() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,47 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| // ApiRequest API 请求实体 | ||||
| type ApiRequest struct { | ||||
| 	Model       string    `json:"model"` | ||||
| 	Temperature float32   `json:"temperature"` | ||||
| 	MaxTokens   int       `json:"max_tokens"` | ||||
| 	Stream      bool      `json:"stream"` | ||||
| 	Messages    []Message `json:"messages"` | ||||
| } | ||||
|  | ||||
| type Message struct { | ||||
| 	Role    string `json:"role"` | ||||
| 	Content string `json:"content"` | ||||
| } | ||||
|  | ||||
| type ApiResponse struct { | ||||
| 	Choices []ChoiceItem `json:"choices"` | ||||
| } | ||||
|  | ||||
| // ChoiceItem API 响应实体 | ||||
| type ChoiceItem struct { | ||||
| 	Delta        Message `json:"delta"` | ||||
| 	FinishReason string  `json:"finish_reason"` | ||||
| } | ||||
|  | ||||
| // ChatSession 聊天会话对象 | ||||
| type ChatSession struct { | ||||
| 	SessionId string `json:"session_id"` | ||||
| 	ClientIP  string `json:"client_ip"` // 客户端 IP | ||||
| 	Username  string `json:"username"`  // 当前登录的 username | ||||
| 	UserId    uint   `json:"user_id"`   // 当前登录的 user ID | ||||
| 	ChatId    string `json:"chat_id"`   // 客户端聊天会话 ID, 多会话模式专用字段 | ||||
| 	Model     string `json:"model"`     // GPT 模型 | ||||
| } | ||||
|  | ||||
| type ApiError struct { | ||||
| 	Error struct { | ||||
| 		Message string | ||||
| 		Type    string | ||||
| 		Param   interface{} | ||||
| 		Code    string | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const PromptMsg = "prompt" // prompt message | ||||
| const ReplyMsg = "reply"   // reply message | ||||
| @@ -1,61 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"sync" | ||||
| ) | ||||
|  | ||||
| var ErrConClosed = errors.New("connection closed") | ||||
|  | ||||
| type Client interface { | ||||
| 	Close() | ||||
| } | ||||
|  | ||||
| // WsClient websocket client | ||||
| type WsClient struct { | ||||
| 	Conn   *websocket.Conn | ||||
| 	lock   sync.Mutex | ||||
| 	mt     int | ||||
| 	closed bool | ||||
| } | ||||
|  | ||||
| func NewWsClient(conn *websocket.Conn) *WsClient { | ||||
| 	return &WsClient{ | ||||
| 		Conn:   conn, | ||||
| 		lock:   sync.Mutex{}, | ||||
| 		mt:     2, // fixed bug for 'Invalid UTF-8 in text frame' | ||||
| 		closed: false, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (wc *WsClient) Send(message []byte) error { | ||||
| 	wc.lock.Lock() | ||||
| 	defer wc.lock.Unlock() | ||||
|  | ||||
| 	if wc.closed { | ||||
| 		return ErrConClosed | ||||
| 	} | ||||
|  | ||||
| 	return wc.Conn.WriteMessage(wc.mt, message) | ||||
| } | ||||
|  | ||||
| func (wc *WsClient) Receive() (int, []byte, error) { | ||||
| 	if wc.closed { | ||||
| 		return 0, nil, ErrConClosed | ||||
| 	} | ||||
|  | ||||
| 	return wc.Conn.ReadMessage() | ||||
| } | ||||
|  | ||||
| func (wc *WsClient) Close() { | ||||
| 	wc.lock.Lock() | ||||
| 	defer wc.lock.Unlock() | ||||
|  | ||||
| 	if wc.closed { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	_ = wc.Conn.Close() | ||||
| 	wc.closed = true | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type AppConfig struct { | ||||
| 	Path     string `toml:"-"` | ||||
| 	Listen   string | ||||
| 	Session  Session | ||||
| 	ProxyURL string | ||||
| 	MysqlDns string  // mysql 连接地址 | ||||
| 	Manager  Manager // 后台管理员账户信息 | ||||
| } | ||||
|  | ||||
| // Manager 管理员 | ||||
| type Manager struct { | ||||
| 	Username string `json:"username"` | ||||
| 	Password string `json:"password"` | ||||
| } | ||||
|  | ||||
| // Session configs struct | ||||
| type Session struct { | ||||
| 	SecretKey string // session encryption key | ||||
| 	Name      string | ||||
| 	Path      string | ||||
| 	Domain    string | ||||
| 	MaxAge    int | ||||
| 	Secure    bool | ||||
| 	HttpOnly  bool | ||||
| 	SameSite  http.SameSite | ||||
| } | ||||
|  | ||||
| // ChatConfig 系统默认的聊天配置 | ||||
| type ChatConfig struct { | ||||
| 	ApiURL        string  `json:"api_url,omitempty"` | ||||
| 	Model         string  `json:"model"` // 默认模型 | ||||
| 	Temperature   float32 `json:"temperature"` | ||||
| 	MaxTokens     int     `json:"max_tokens"` | ||||
| 	EnableContext bool    `json:"enable_context"` // 是否开启聊天上下文 | ||||
| 	EnableHistory bool    `json:"enable_history"` // 是否允许保存聊天记录 | ||||
| 	ApiKey        string  `json:"api_key"`        // OpenAI  API key | ||||
| } | ||||
|  | ||||
| type SystemConfig struct { | ||||
| 	Title         string   `json:"title"` | ||||
| 	AdminTitle    string   `json:"admin_title"` | ||||
| 	Models        []string `json:"models"` | ||||
| 	UserInitCalls int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用 | ||||
| } | ||||
|  | ||||
| const UserInitCalls = 1000 | ||||
| @@ -1,6 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| const SessionName = "ChatGPT-TOKEN" | ||||
| const SessionUser = "SESSION_USER"        // 存储用户信息的 session key | ||||
| const SessionAdmin = "SESSION_ADMIN"      //存储管理员信息的 session key | ||||
| const LoginUserCache = "LOGIN_USER_CACHE" // 已登录用户缓存 | ||||
| @@ -1,36 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| // BizVo 业务返回 VO | ||||
| type BizVo struct { | ||||
| 	Code     BizCode     `json:"code"` | ||||
| 	Page     int         `json:"page,omitempty"` | ||||
| 	PageSize int         `json:"page_size,omitempty"` | ||||
| 	Total    int         `json:"total,omitempty"` | ||||
| 	Message  string      `json:"message,omitempty"` | ||||
| 	Data     interface{} `json:"data,omitempty"` | ||||
| } | ||||
|  | ||||
| // WsMessage Websocket message | ||||
| type WsMessage struct { | ||||
| 	Type    WsMsgType `json:"type"` // 消息类别,start, end | ||||
| 	Content string    `json:"content"` | ||||
| } | ||||
| type WsMsgType string | ||||
|  | ||||
| const ( | ||||
| 	WsStart  = WsMsgType("start") | ||||
| 	WsMiddle = WsMsgType("middle") | ||||
| 	WsEnd    = WsMsgType("end") | ||||
| ) | ||||
|  | ||||
| type BizCode int | ||||
|  | ||||
| const ( | ||||
| 	Success       = BizCode(0) | ||||
| 	Failed        = BizCode(1) | ||||
| 	NotAuthorized = BizCode(400) // 未授权 | ||||
|  | ||||
| 	OkMsg       = "Success" | ||||
| 	ErrorMsg    = "系统开小差了" | ||||
| 	InvalidArgs = "非法参数或参数解析失败" | ||||
| ) | ||||
| @@ -1,59 +0,0 @@ | ||||
| module chatplus | ||||
|  | ||||
| go 1.19 | ||||
|  | ||||
| require ( | ||||
| 	github.com/BurntSushi/toml v1.1.0 | ||||
| 	github.com/gin-contrib/sessions v0.0.5 | ||||
| 	github.com/gin-gonic/gin v1.9.0 | ||||
| 	github.com/gorilla/websocket v1.5.0 | ||||
| 	github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 | ||||
| 	github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 | ||||
| 	github.com/syndtr/goleveldb v1.0.0 | ||||
| 	go.uber.org/zap v1.23.0 | ||||
| 	gorm.io/driver/mysql v1.4.7 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/bytedance/sonic v1.8.0 // indirect | ||||
| 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect | ||||
| 	github.com/dlclark/regexp2 v1.8.1 // indirect | ||||
| 	github.com/go-sql-driver/mysql v1.7.0 // indirect | ||||
| 	github.com/goccy/go-json v0.10.0 // indirect | ||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||
| 	github.com/jinzhu/now v1.1.5 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.0.9 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	go.uber.org/dig v1.16.1 // indirect | ||||
| 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect | ||||
| 	golang.org/x/net v0.7.0 // indirect | ||||
| 	golang.org/x/text v0.7.0 // indirect | ||||
| 	google.golang.org/protobuf v1.28.1 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.11.2 // indirect | ||||
| 	github.com/golang/protobuf v1.5.0 // indirect | ||||
| 	github.com/golang/snappy v0.0.1 // indirect | ||||
| 	github.com/gorilla/context v1.1.1 // indirect | ||||
| 	github.com/gorilla/securecookie v1.1.1 // indirect | ||||
| 	github.com/gorilla/sessions v1.2.1 // indirect | ||||
| 	github.com/json-iterator/go v1.1.12 // indirect | ||||
| 	github.com/leodido/go-urn v1.2.1 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.17 // indirect | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.9 // indirect | ||||
| 	go.uber.org/atomic v1.7.0 // indirect | ||||
| 	go.uber.org/fx v1.19.3 | ||||
| 	go.uber.org/multierr v1.6.0 // indirect | ||||
| 	golang.org/x/crypto v0.6.0 | ||||
| 	golang.org/x/sys v0.5.0 // indirect | ||||
| 	gopkg.in/yaml.v2 v2.2.8 // indirect | ||||
| 	gorm.io/gorm v1.25.1 | ||||
| ) | ||||
							
								
								
									
										177
									
								
								api/go/go.sum
									
									
									
									
									
								
							
							
						
						
									
										177
									
								
								api/go/go.sum
									
									
									
									
									
								
							| @@ -1,177 +0,0 @@ | ||||
| github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= | ||||
| github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | ||||
| github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= | ||||
| github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | ||||
| github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= | ||||
| github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= | ||||
| github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | ||||
| github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||||
| github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= | ||||
| github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= | ||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||
| github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= | ||||
| github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= | ||||
| github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= | ||||
| github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= | ||||
| github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= | ||||
| github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= | ||||
| github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= | ||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||
| github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= | ||||
| github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= | ||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||
| github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= | ||||
| github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= | ||||
| github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= | ||||
| github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= | ||||
| github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= | ||||
| github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||
| github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= | ||||
| github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= | ||||
| github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= | ||||
| github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||||
| github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||
| github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= | ||||
| github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | ||||
| github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | ||||
| github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | ||||
| github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||
| github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | ||||
| github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||
| github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||
| github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||
| github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= | ||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= | ||||
| github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= | ||||
| github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= | ||||
| github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= | ||||
| github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs= | ||||
| github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs= | ||||
| github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= | ||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||
| github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= | ||||
| github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= | ||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||
| github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= | ||||
| github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= | ||||
| github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||
| github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= | ||||
| github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= | ||||
| github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= | ||||
| github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||
| github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | ||||
| github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= | ||||
| github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= | ||||
| github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= | ||||
| github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||
| go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= | ||||
| go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= | ||||
| go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= | ||||
| go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA= | ||||
| go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM= | ||||
| go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= | ||||
| go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= | ||||
| go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= | ||||
| go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= | ||||
| go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= | ||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= | ||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= | ||||
| golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= | ||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= | ||||
| golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= | ||||
| golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= | ||||
| golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||
| golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= | ||||
| golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||
| google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= | ||||
| google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= | ||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= | ||||
| gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= | ||||
| gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= | ||||
| gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= | ||||
| gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= | ||||
| gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= | ||||
| rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | ||||
| @@ -1,111 +0,0 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/handler" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-contrib/sessions" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| type ManagerHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewAdminHandler(app *core.AppServer, db *gorm.DB) *ManagerHandler { | ||||
| 	h := ManagerHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| // Login 登录 | ||||
| func (h *ManagerHandler) Login(c *gin.Context) { | ||||
| 	var data types.Manager | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	manager := h.App.AppConfig.Manager | ||||
| 	if data.Username == manager.Username && data.Password == manager.Password { | ||||
| 		err := utils.SetLoginAdmin(c, manager) | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, "Save session failed") | ||||
| 			return | ||||
| 		} | ||||
| 		manager.Password = "" // 清空密码] | ||||
| 		resp.SUCCESS(c, manager) | ||||
| 	} else { | ||||
| 		resp.ERROR(c, "用户名或者密码错误") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Logout 注销 | ||||
| func (h *ManagerHandler) Logout(c *gin.Context) { | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Delete(types.SessionAdmin) | ||||
| 	err := session.Save() | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "Save session failed") | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Session 会话检测 | ||||
| func (h *ManagerHandler) Session(c *gin.Context) { | ||||
| 	session := sessions.Default(c) | ||||
| 	admin := session.Get(types.SessionAdmin) | ||||
| 	if admin == nil { | ||||
| 		resp.NotAuth(c) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Migrate 数据修正 | ||||
| func (h *ManagerHandler) Migrate(c *gin.Context) { | ||||
| 	opt := c.Query("opt") | ||||
| 	switch opt { | ||||
| 	case "user": | ||||
| 		// 将用户订阅角色的数据结构从 map 改成数组 | ||||
| 		var users []model.User | ||||
| 		h.db.Find(&users) | ||||
| 		for _, u := range users { | ||||
| 			var m map[string]int | ||||
| 			var roleKeys = make([]string, 0) | ||||
| 			err := utils.JsonDecode(u.ChatRoles, &m) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			for k, _ := range m { | ||||
| 				roleKeys = append(roleKeys, k) | ||||
| 			} | ||||
| 			u.ChatRoles = utils.JsonEncode(roleKeys) | ||||
| 			h.db.Updates(&u) | ||||
|  | ||||
| 		} | ||||
| 		break | ||||
| 	case "role": | ||||
| 		// TestRole 修改角色图片,改成绝对路径 | ||||
| 		var roles []model.ChatRole | ||||
| 		h.db.Find(&roles) | ||||
| 		for _, r := range roles { | ||||
| 			r.Icon = "/" + r.Icon | ||||
| 			h.db.Updates(&r) | ||||
| 		} | ||||
| 		break | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, "SUCCESS") | ||||
| } | ||||
| @@ -1,100 +0,0 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ApiKeyHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler { | ||||
| 	h := ApiKeyHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| func (h *ApiKeyHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id         uint   `json:"id"` | ||||
| 		UserId     uint   `json:"user_id"` | ||||
| 		Value      string `json:"value"` | ||||
| 		LastUsedAt string `json:"last_used_at"` | ||||
| 		CreatedAt  int64  `json:"created_at"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	apiKey := model.ApiKey{Value: data.Value, UserId: data.UserId, LastUsedAt: utils.Str2stamp(data.LastUsedAt)} | ||||
| 	apiKey.Id = data.Id | ||||
| 	if apiKey.Id > 0 { | ||||
| 		apiKey.CreatedAt = time.Unix(data.CreatedAt, 0) | ||||
| 	} | ||||
| 	res := h.db.Save(&apiKey) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var keyVo vo.ApiKey | ||||
| 	err := utils.CopyObject(apiKey, &keyVo) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "数据拷贝失败!") | ||||
| 		return | ||||
| 	} | ||||
| 	keyVo.Id = apiKey.Id | ||||
| 	keyVo.CreatedAt = apiKey.CreatedAt.Unix() | ||||
| 	resp.SUCCESS(c, keyVo) | ||||
| } | ||||
|  | ||||
| func (h *ApiKeyHandler) List(c *gin.Context) { | ||||
| 	userId := h.GetInt(c, "user_id", -1) | ||||
| 	query := h.db.Session(&gorm.Session{}) | ||||
| 	if userId >= 0 { | ||||
| 		query = query.Where("user_id", userId) | ||||
| 	} | ||||
| 	var items []model.ApiKey | ||||
| 	var keys = make([]vo.ApiKey, 0) | ||||
| 	res := query.Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var key vo.ApiKey | ||||
| 			err := utils.CopyObject(item, &key) | ||||
| 			if err == nil { | ||||
| 				key.Id = item.Id | ||||
| 				key.CreatedAt = item.CreatedAt.Unix() | ||||
| 				key.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				keys = append(keys, key) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, keys) | ||||
| } | ||||
|  | ||||
| func (h *ApiKeyHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
|  | ||||
| 	if id > 0 { | ||||
| 		res := h.db.Where("id = ?", id).Delete(&model.ApiKey{}) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, "更新数据库失败!") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
| @@ -1,114 +0,0 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type ChatRoleHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler { | ||||
| 	h := ChatRoleHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| // Save 创建或者更新某个角色 | ||||
| func (h *ChatRoleHandler) Save(c *gin.Context) { | ||||
| 	var data vo.ChatRole | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	var role model.ChatRole | ||||
| 	err := utils.CopyObject(data, &role) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	role.Id = data.Id | ||||
| 	if data.CreatedAt > 0 { | ||||
| 		role.CreatedAt = time.Unix(data.CreatedAt, 0) | ||||
| 	} | ||||
| 	res := h.db.Save(&role) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| 		return | ||||
| 	} | ||||
| 	// 填充 ID 数据 | ||||
| 	data.Id = role.Id | ||||
| 	data.CreatedAt = role.CreatedAt.Unix() | ||||
| 	resp.SUCCESS(c, data) | ||||
| } | ||||
|  | ||||
| func (h *ChatRoleHandler) List(c *gin.Context) { | ||||
| 	var items []model.ChatRole | ||||
| 	var roles = make([]vo.ChatRole, 0) | ||||
| 	res := h.db.Order("sort ASC").Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No data found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range items { | ||||
| 		var role vo.ChatRole | ||||
| 		err := utils.CopyObject(v, &role) | ||||
| 		if err == nil { | ||||
| 			role.Id = v.Id | ||||
| 			role.CreatedAt = v.CreatedAt.Unix() | ||||
| 			role.UpdatedAt = v.UpdatedAt.Unix() | ||||
| 			roles = append(roles, role) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, roles) | ||||
| } | ||||
|  | ||||
| // SetSort 更新角色排序 | ||||
| func (h *ChatRoleHandler) SetSort(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id   uint `json:"id"` | ||||
| 		Sort int  `json:"sort"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	if data.Id <= 0 { | ||||
| 		resp.HACKER(c) | ||||
| 		return | ||||
| 	} | ||||
| 	res := h.db.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update("sort", data.Sort) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ChatRoleHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	if id <= 0 { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res := h.db.Where("id = ?", id).Delete(&model.ChatRole{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "删除失败!") | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
| @@ -1,74 +0,0 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ConfigHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler { | ||||
| 	h := ConfigHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| func (h *ConfigHandler) Update(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Key    string                 `json:"key"` | ||||
| 		Config map[string]interface{} `json:"config"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	str := utils.JsonEncode(&data.Config) | ||||
| 	config := model.Config{Key: data.Key, Config: str} | ||||
| 	res := h.db.FirstOrCreate(&config, model.Config{Key: data.Key}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if config.Id > 0 { | ||||
| 		config.Config = str | ||||
| 		res := h.db.Updates(&config) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, config) | ||||
| } | ||||
|  | ||||
| // Get 获取指定的系统配置 | ||||
| func (h *ConfigHandler) Get(c *gin.Context) { | ||||
| 	key := c.Query("key") | ||||
| 	var config model.Config | ||||
| 	res := h.db.Where("marker", key).First(&config) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var m map[string]interface{} | ||||
| 	err := utils.JsonDecode(config.Config, &m) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, m) | ||||
| } | ||||
| @@ -1,145 +0,0 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type UserHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler { | ||||
| 	h := UserHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| // List 用户列表 | ||||
| func (h *UserHandler) List(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 1) | ||||
| 	pageSize := h.GetInt(c, "page_size", 20) | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	var items []model.User | ||||
| 	var users = make([]vo.User, 0) | ||||
| 	var total int64 | ||||
| 	h.db.Model(&model.User{}).Count(&total) | ||||
| 	res := h.db.Offset(offset).Limit(pageSize).Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var user vo.User | ||||
| 			err := utils.CopyObject(item, &user) | ||||
| 			if err == nil { | ||||
| 				user.Id = item.Id | ||||
| 				user.CreatedAt = item.CreatedAt.Unix() | ||||
| 				user.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				users = append(users, user) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	pageVo := vo.NewPage(total, page, pageSize, users) | ||||
| 	resp.SUCCESS(c, pageVo) | ||||
| } | ||||
|  | ||||
| func (h *UserHandler) Update(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id          uint     `json:"id"` | ||||
| 		Nickname    string   `json:"nickname"` | ||||
| 		Calls       int      `json:"calls"` | ||||
| 		ChatRoles   []string `json:"chat_roles"` | ||||
| 		ExpiredTime string   `json:"expired_time"` | ||||
| 		Status      bool     `json:"status"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	var user = model.User{} | ||||
| 	user.Id = data.Id | ||||
| 	// 此处需要用 map 更新,用结构体无法更新 0 值 | ||||
| 	res := h.db.Model(&user).Updates(map[string]interface{}{ | ||||
| 		"nickname":        data.Nickname, | ||||
| 		"calls":           data.Calls, | ||||
| 		"status":          data.Status, | ||||
| 		"chat_roles_json": utils.JsonEncode(data.ChatRoles), | ||||
| 		"expired_time":    utils.Str2stamp(data.ExpiredTime), | ||||
| 	}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *UserHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	if id > 0 { | ||||
| 		tx := h.db.Begin() | ||||
| 		res := h.db.Where("id = ?", id).Delete(&model.User{}) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, "删除失败") | ||||
| 			return | ||||
| 		} | ||||
| 		// 删除聊天记录 | ||||
| 		res = h.db.Where("user_id = ?", id).Delete(&model.ChatItem{}) | ||||
| 		if res.Error != nil { | ||||
| 			tx.Rollback() | ||||
| 			resp.ERROR(c, "删除失败") | ||||
| 			return | ||||
| 		} | ||||
| 		// 删除聊天历史记录 | ||||
| 		res = h.db.Where("user_id = ?", id).Delete(&model.HistoryMessage{}) | ||||
| 		if res.Error != nil { | ||||
| 			tx.Rollback() | ||||
| 			resp.ERROR(c, "删除失败") | ||||
| 			return | ||||
| 		} | ||||
| 		// 删除登录日志 | ||||
| 		res = h.db.Where("user_id = ?", id).Delete(&model.UserLoginLog{}) | ||||
| 		if res.Error != nil { | ||||
| 			tx.Rollback() | ||||
| 			resp.ERROR(c, "删除失败") | ||||
| 			return | ||||
| 		} | ||||
| 		tx.Commit() | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *UserHandler) LoginLog(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 1) | ||||
| 	pageSize := h.GetInt(c, "page_size", 20) | ||||
| 	var total int64 | ||||
| 	h.db.Model(&model.UserLoginLog{}).Count(&total) | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	var items []model.UserLoginLog | ||||
| 	res := h.db.Offset(offset).Limit(pageSize).Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "获取数据失败") | ||||
| 		return | ||||
| 	} | ||||
| 	var logs []vo.UserLoginLog | ||||
| 	for _, v := range items { | ||||
| 		var log vo.UserLoginLog | ||||
| 		err := utils.CopyObject(v, &log) | ||||
| 		if err == nil { | ||||
| 			log.Id = v.Id | ||||
| 			log.CreatedAt = v.CreatedAt.Unix() | ||||
| 			logs = append(logs, log) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs)) | ||||
| } | ||||
| @@ -1,66 +0,0 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| type BaseHandler struct { | ||||
| 	App *core.AppServer | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetTrim(c *gin.Context, key string) string { | ||||
| 	return strings.TrimSpace(c.Query(key)) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int { | ||||
| 	return intValue(c.PostForm(key), defaultValue) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int { | ||||
| 	return intValue(c.Query(key), defaultValue) | ||||
| } | ||||
|  | ||||
| func intValue(str string, defaultValue int) int { | ||||
| 	value, err := strconv.Atoi(str) | ||||
| 	if err != nil { | ||||
| 		return defaultValue | ||||
| 	} | ||||
| 	return value | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 { | ||||
| 	return floatValue(c.Query(key)) | ||||
| } | ||||
| func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 { | ||||
| 	return floatValue(c.PostForm(key)) | ||||
| } | ||||
|  | ||||
| func floatValue(str string) float64 { | ||||
| 	value, err := strconv.ParseFloat(str, 64) | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return value | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetBool(c *gin.Context, key string) bool { | ||||
| 	return boolValue(c.Query(key)) | ||||
| } | ||||
| func (h *BaseHandler) PostBool(c *gin.Context, key string) bool { | ||||
| 	return boolValue(c.PostForm(key)) | ||||
| } | ||||
|  | ||||
| func boolValue(str string) bool { | ||||
| 	value, err := strconv.ParseBool(str) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	return value | ||||
| } | ||||
| @@ -1,453 +0,0 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| const ErrorMsg = "抱歉,AI 助手开小差了,请马上联系管理员去盘它。" | ||||
|  | ||||
| type ChatHandler struct { | ||||
| 	BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler { | ||||
| 	handler := ChatHandler{db: db} | ||||
| 	handler.App = app | ||||
| 	return &handler | ||||
| } | ||||
|  | ||||
| // ChatHandle 处理聊天 WebSocket 请求 | ||||
| func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 	ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		return | ||||
| 	} | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	roleId := h.GetInt(c, "role_id", 0) | ||||
| 	chatId := c.Query("chat_id") | ||||
| 	chatModel := c.Query("model") | ||||
|  | ||||
| 	session := h.App.ChatSession.Get(sessionId) | ||||
| 	if session.SessionId == "" { | ||||
| 		logger.Info("用户未登录") | ||||
| 		c.Abort() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// use old chat data override the chat model and role ID | ||||
| 	var chat model.ChatItem | ||||
| 	res := h.db.Where("chat_id=?", chatId).First(&chat) | ||||
| 	if res.Error == nil { | ||||
| 		chatModel = chat.Model | ||||
| 		roleId = int(chat.RoleId) | ||||
| 	} | ||||
|  | ||||
| 	session.ChatId = chatId | ||||
| 	session.Model = chatModel | ||||
| 	logger.Infof("New websocket connected, IP: %s, Username: %s", c.Request.RemoteAddr, session.Username) | ||||
| 	client := types.NewWsClient(ws) | ||||
| 	var chatRole model.ChatRole | ||||
| 	res = h.db.First(&chatRole, roleId) | ||||
| 	if res.Error != nil || !chatRole.Enable { | ||||
| 		replyMessage(client, "当前聊天角色不存在或者未启用!!!") | ||||
| 		c.Abort() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 保存会话连接 | ||||
| 	h.App.ChatClients.Put(sessionId, client) | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			_, message, err := client.Receive() | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 				client.Close() | ||||
| 				h.App.ChatClients.Delete(sessionId) | ||||
| 				h.App.ReqCancelFunc.Delete(sessionId) | ||||
| 				return | ||||
| 			} | ||||
| 			logger.Info("Receive a message: ", string(message)) | ||||
| 			//replyMessage(client, "这是一条测试消息!") | ||||
| 			ctx, cancel := context.WithCancel(context.Background()) | ||||
| 			h.App.ReqCancelFunc.Put(sessionId, cancel) | ||||
| 			// 回复消息 | ||||
| 			err = h.sendMessage(ctx, session, chatRole, string(message), client) | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 			} else { | ||||
| 				replyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) | ||||
| 				logger.Info("回答完毕: " + string(message)) | ||||
| 			} | ||||
|  | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
|  | ||||
| // 将消息发送给 ChatGPT 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession, role model.ChatRole, prompt string, ws types.Client) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
|  | ||||
| 	var user model.User | ||||
| 	res := h.db.Model(&model.User{}).First(&user, session.UserId) | ||||
| 	if res.Error != nil { | ||||
| 		replyMessage(ws, "非法用户,请联系管理员!") | ||||
| 		return res.Error | ||||
| 	} | ||||
| 	var userVo vo.User | ||||
| 	err := utils.CopyObject(user, &userVo) | ||||
| 	userVo.Id = user.Id | ||||
| 	if err != nil { | ||||
| 		return errors.New("User 对象转换失败," + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Status == false { | ||||
| 		replyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!") | ||||
| 		replyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Calls <= 0 { | ||||
| 		replyMessage(ws, "您的对话次数已经用尽,请联系管理员充值!") | ||||
| 		replyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() { | ||||
| 		replyMessage(ws, "您的账号已经过期,请联系管理员!") | ||||
| 		replyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
| 	var req = types.ApiRequest{ | ||||
| 		Model:       session.Model, | ||||
| 		Temperature: userVo.ChatConfig.Temperature, | ||||
| 		MaxTokens:   userVo.ChatConfig.MaxTokens, | ||||
| 		Stream:      true, | ||||
| 	} | ||||
|  | ||||
| 	// 加载聊天上下文 | ||||
| 	var chatCtx []types.Message | ||||
| 	if userVo.ChatConfig.EnableContext { | ||||
| 		if h.App.ChatContexts.Has(session.ChatId) { | ||||
| 			chatCtx = h.App.ChatContexts.Get(session.ChatId) | ||||
| 		} else { | ||||
| 			// 加载角色信息 | ||||
| 			var messages []types.Message | ||||
| 			err := utils.JsonDecode(role.Context, &messages) | ||||
| 			if err == nil { | ||||
| 				chatCtx = messages | ||||
| 			} | ||||
| 			// TODO: 这里默认加载最近 4 条聊天记录作为上下文,后期应该做成可配置的 | ||||
| 			var historyMessages []model.HistoryMessage | ||||
| 			res := h.db.Where("chat_id = ?", session.ChatId).Limit(4).Order("created_at desc").Find(&historyMessages) | ||||
| 			if res.Error == nil { | ||||
| 				for _, msg := range historyMessages { | ||||
| 					ms := types.Message{Role: "user", Content: msg.Content} | ||||
| 					if msg.Type == types.ReplyMsg { | ||||
| 						ms.Role = "assistant" | ||||
| 					} | ||||
| 					chatCtx = append(chatCtx, ms) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		logger.Info("聊天上下文:", chatCtx) | ||||
| 	} | ||||
| 	req.Messages = append(chatCtx, types.Message{ | ||||
| 		Role:    "user", | ||||
| 		Content: prompt, | ||||
| 	}) | ||||
| 	var apiKey string | ||||
| 	response, err := h.doRequest(ctx, userVo, &apiKey, req) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			logger.Info("用户取消了请求:", prompt) | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		replyMessage(ws, ErrorMsg) | ||||
| 		replyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var responseBody = types.ApiResponse{} | ||||
| 		reader := bufio.NewReader(response.Body) | ||||
| 		for { | ||||
| 			line, err := reader.ReadString('\n') | ||||
| 			if err != nil { | ||||
| 				if strings.Contains(err.Error(), "context canceled") { | ||||
| 					logger.Info("用户取消了请求:", prompt) | ||||
| 				} else { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				break | ||||
| 			} | ||||
| 			if !strings.Contains(line, "data:") { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			err = json.Unmarshal([]byte(line[6:]), &responseBody) | ||||
| 			if err != nil { // 数据解析出错 | ||||
| 				logger.Error(err, line) | ||||
| 				replyMessage(ws, ErrorMsg) | ||||
| 				replyMessage(ws, "") | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// 初始化 role | ||||
| 			if responseBody.Choices[0].Delta.Role != "" && message.Role == "" { | ||||
| 				message.Role = responseBody.Choices[0].Delta.Role | ||||
| 				replyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 				continue | ||||
| 			} else if responseBody.Choices[0].FinishReason != "" { | ||||
| 				break // 输出完成或者输出中断了 | ||||
| 			} else { | ||||
| 				content := responseBody.Choices[0].Delta.Content | ||||
| 				contents = append(contents, content) | ||||
| 				replyChunkMessage(ws, types.WsMessage{ | ||||
| 					Type:    types.WsMiddle, | ||||
| 					Content: responseBody.Choices[0].Delta.Content, | ||||
| 				}) | ||||
| 			} | ||||
| 		} // end for | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			// 更新用户的对话次数 | ||||
| 			res := h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls - ?", 1)) | ||||
| 			if res.Error != nil { | ||||
| 				return res.Error | ||||
| 			} | ||||
|  | ||||
| 			if message.Role == "" { | ||||
| 				message.Role = "assistant" | ||||
| 			} | ||||
| 			message.Content = strings.Join(contents, "") | ||||
| 			useMsg := types.Message{Role: "user", Content: prompt} | ||||
|  | ||||
| 			// 更新上下文消息 | ||||
| 			if userVo.ChatConfig.EnableContext { | ||||
| 				chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 				chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 				h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 			} | ||||
|  | ||||
| 			// 追加聊天记录 | ||||
| 			if userVo.ChatConfig.EnableHistory { | ||||
| 				// for prompt | ||||
| 				token, err := utils.CalcTokens(prompt, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyUserMsg := model.HistoryMessage{ | ||||
| 					UserId:  userVo.Id, | ||||
| 					ChatId:  session.ChatId, | ||||
| 					RoleId:  role.Id, | ||||
| 					Type:    types.PromptMsg, | ||||
| 					Icon:    user.Avatar, | ||||
| 					Content: prompt, | ||||
| 					Tokens:  token, | ||||
| 				} | ||||
| 				historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 				historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 				res := h.db.Save(&historyUserMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save prompt history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// for reply | ||||
| 				token, err = utils.CalcTokens(message.Content, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:  userVo.Id, | ||||
| 					ChatId:  session.ChatId, | ||||
| 					RoleId:  role.Id, | ||||
| 					Type:    types.ReplyMsg, | ||||
| 					Icon:    role.Icon, | ||||
| 					Content: message.Content, | ||||
| 					Tokens:  token, | ||||
| 				} | ||||
| 				historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 				historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 				res = h.db.Create(&historyReplyMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save reply history message: ", res.Error) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// 保存当前会话 | ||||
| 			var chatItem model.ChatItem | ||||
| 			res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) | ||||
| 			if res.Error != nil { | ||||
| 				chatItem.ChatId = session.ChatId | ||||
| 				chatItem.UserId = session.UserId | ||||
| 				chatItem.RoleId = role.Id | ||||
| 				chatItem.Model = session.Model | ||||
| 				if utf8.RuneCountInString(prompt) > 30 { | ||||
| 					chatItem.Title = string([]rune(prompt)[:30]) + "..." | ||||
| 				} else { | ||||
| 					chatItem.Title = prompt | ||||
| 				} | ||||
| 				h.db.Create(&chatItem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with reading response: %v", err) | ||||
| 		} | ||||
| 		var res types.ApiError | ||||
| 		err = json.Unmarshal(body, &res) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with decode response: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		// OpenAI API 调用异常处理 | ||||
| 		// TODO: 是否考虑重发消息? | ||||
| 		if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") { | ||||
| 			replyMessage(ws, "请求 OpenAI API 失败:API KEY 所关联的账户被禁用。") | ||||
| 			// 移除当前 API key | ||||
| 			h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{}) | ||||
| 		} else if strings.Contains(res.Error.Message, "You exceeded your current quota") { | ||||
| 			replyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。") | ||||
| 		} else if strings.Contains(res.Error.Message, "This model's maximum context length") { | ||||
| 			replyMessage(ws, "当前会话上下文长度超出限制,已为您删减会话上下文!") | ||||
| 			// 只保留最近的三条记录 | ||||
| 			chatContext := h.App.ChatContexts.Get(session.ChatId) | ||||
| 			if len(chatContext) > 3 { | ||||
| 				chatContext = chatContext[len(chatContext)-3:] | ||||
| 			} | ||||
| 			h.App.ChatContexts.Put(session.ChatId, chatContext) | ||||
| 			return h.sendMessage(ctx, session, role, prompt, ws) | ||||
| 		} else { | ||||
| 			replyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // 发送请求到 OpenAI 服务器 | ||||
| // useOwnApiKey: 是否使用了用户自己的 API KEY | ||||
| func (h *ChatHandler) doRequest(ctx context.Context, user vo.User, apiKey *string, req types.ApiRequest) (*http.Response, error) { | ||||
| 	var client *http.Client | ||||
| 	requestBody, err := json.Marshal(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// 创建 HttpClient 请求对象 | ||||
| 	request, err := http.NewRequest(http.MethodPost, h.App.ChatConfig.ApiURL, bytes.NewBuffer(requestBody)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	request = request.WithContext(ctx) | ||||
| 	request.Header.Add("Content-Type", "application/json") | ||||
|  | ||||
| 	proxyURL := h.App.AppConfig.ProxyURL | ||||
| 	if proxyURL == "" { | ||||
| 		client = &http.Client{} | ||||
| 	} else { // 使用代理 | ||||
| 		uri := url.URL{} | ||||
| 		proxy, _ := uri.Parse(proxyURL) | ||||
| 		client = &http.Client{ | ||||
| 			Transport: &http.Transport{ | ||||
| 				Proxy: http.ProxyURL(proxy), | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
| 	// 查询当前用户是否导入了自己的 API KEY | ||||
| 	if user.ChatConfig.ApiKey != "" { | ||||
| 		logger.Info("使用用户自己的 API KEY: ", user.ChatConfig.ApiKey) | ||||
| 		*apiKey = user.ChatConfig.ApiKey | ||||
| 	} else { // 获取系统的 API KEY | ||||
| 		var key model.ApiKey | ||||
| 		res := h.db.Where("user_id = ?", 0).Order("last_used_at ASC").First(&key) | ||||
| 		if res.Error != nil { | ||||
| 			return nil, errors.New("no available key, please import key") | ||||
| 		} | ||||
| 		*apiKey = key.Value | ||||
| 		// 更新 API KEY 的最后使用时间 | ||||
| 		h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix()) | ||||
| 	} | ||||
|  | ||||
| 	logger.Infof("Sending OpenAI request, KEY: %s, PROXY: %s, Model: %s", *apiKey, proxyURL, req.Model) | ||||
| 	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey)) | ||||
| 	return client.Do(request) | ||||
| } | ||||
|  | ||||
| // 回复客户片段端消息 | ||||
| func replyChunkMessage(client types.Client, message types.WsMessage) { | ||||
| 	msg, err := json.Marshal(message) | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error for decoding json data: %v", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	err = client.(*types.WsClient).Send(msg) | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error for reply message: %v", err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 回复客户端一条完整的消息 | ||||
| func replyMessage(ws types.Client, message string) { | ||||
| 	replyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 	replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message}) | ||||
| 	replyChunkMessage(ws, types.WsMessage{Type: types.WsEnd}) | ||||
| } | ||||
|  | ||||
| // Tokens 统计 token 数量 | ||||
| func (h *ChatHandler) Tokens(c *gin.Context) { | ||||
| 	text := c.Query("text") | ||||
| 	md := c.Query("model") | ||||
| 	tokens, err := utils.CalcTokens(text, md) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, tokens) | ||||
| } | ||||
|  | ||||
| // StopGenerate 停止生成 | ||||
| func (h *ChatHandler) StopGenerate(c *gin.Context) { | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	if h.App.ReqCancelFunc.Has(sessionId) { | ||||
| 		h.App.ReqCancelFunc.Get(sessionId)() | ||||
| 		h.App.ReqCancelFunc.Delete(sessionId) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
| @@ -1,157 +0,0 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // List 获取会话列表 | ||||
| func (h *ChatHandler) List(c *gin.Context) { | ||||
| 	userId := h.GetInt(c, "user_id", 0) | ||||
| 	if userId == 0 { | ||||
| 		resp.ERROR(c, "The parameter 'user_id' is needed.") | ||||
| 		return | ||||
| 	} | ||||
| 	var items = make([]vo.ChatItem, 0) | ||||
| 	var chats []model.ChatItem | ||||
| 	res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats) | ||||
| 	if res.Error == nil { | ||||
| 		var roleIds = make([]uint, 0) | ||||
| 		for _, chat := range chats { | ||||
| 			roleIds = append(roleIds, chat.RoleId) | ||||
| 		} | ||||
| 		var roles []model.ChatRole | ||||
| 		res = h.db.Find(&roles, roleIds) | ||||
| 		if res.Error == nil { | ||||
| 			roleMap := make(map[uint]model.ChatRole) | ||||
| 			for _, role := range roles { | ||||
| 				roleMap[role.Id] = role | ||||
| 			} | ||||
|  | ||||
| 			for _, chat := range chats { | ||||
| 				var item vo.ChatItem | ||||
| 				err := utils.CopyObject(chat, &item) | ||||
| 				if err == nil { | ||||
| 					item.Id = chat.Id | ||||
| 					item.Icon = roleMap[chat.RoleId].Icon | ||||
| 					items = append(items, item) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
| 	resp.SUCCESS(c, items) | ||||
| } | ||||
|  | ||||
| // Update 更新会话标题 | ||||
| func (h *ChatHandler) Update(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id    uint   `json:"id"` | ||||
| 		Title string `json:"title"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	var m = model.ChatItem{} | ||||
| 	m.Id = data.Id | ||||
| 	res := h.db.Model(&m).UpdateColumn("title", data.Title) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // Remove 删除会话 | ||||
| func (h *ChatHandler) Remove(c *gin.Context) { | ||||
| 	chatId := h.GetTrim(c, "chat_id") | ||||
| 	if chatId == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res := h.db.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 清空会话上下文 | ||||
| 	h.App.ChatContexts.Delete(chatId) | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // History 获取聊天历史记录 | ||||
| func (h *ChatHandler) History(c *gin.Context) { | ||||
| 	chatId := c.Query("chat_id") // 会话 ID | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
| 	var items []model.HistoryMessage | ||||
| 	var messages = make([]vo.HistoryMessage, 0) | ||||
| 	res := h.db.Where("chat_id = ? AND user_id = ?", chatId, user.Id).Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No history message") | ||||
| 		return | ||||
| 	} else { | ||||
| 		for _, item := range items { | ||||
| 			var v vo.HistoryMessage | ||||
| 			err := utils.CopyObject(item, &v) | ||||
| 			v.CreatedAt = item.CreatedAt.Unix() | ||||
| 			v.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 			if err == nil { | ||||
| 				messages = append(messages, v) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, messages) | ||||
| } | ||||
|  | ||||
| // Clear 清空所有聊天记录 | ||||
| func (h *ChatHandler) Clear(c *gin.Context) { | ||||
| 	// 获取当前登录用户所有的聊天会话 | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chats []model.ChatItem | ||||
| 	res := h.db.Where("user_id = ?", user.Id).Find(&chats) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No chats found") | ||||
| 		return | ||||
| 	} | ||||
| 	// 清空聊天记录 | ||||
| 	for _, chat := range chats { | ||||
| 		err := h.db.Where("chat_id = ? AND user_id = ?", chat.ChatId, user.Id).Delete(&model.HistoryMessage{}) | ||||
| 		if err != nil { | ||||
| 			logger.Warnf("Failed to delele chat history for ChatID: %s", chat.ChatId) | ||||
| 		} | ||||
| 		// 清空会话上下文 | ||||
| 		h.App.ChatContexts.Delete(chat.ChatId) | ||||
| 	} | ||||
| 	// 删除所有的会话记录 | ||||
| 	res = h.db.Where("user_id = ?", user.Id).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to remove chat from database.") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatRoleHandler struct { | ||||
| 	BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler { | ||||
| 	handler := &ChatRoleHandler{db: db} | ||||
| 	handler.App = app | ||||
| 	return handler | ||||
| } | ||||
|  | ||||
| // List get user list | ||||
| func (h *ChatRoleHandler) List(c *gin.Context) { | ||||
| 	var roles []model.ChatRole | ||||
| 	res := h.db.Where("enable", true).Order("sort ASC").Find(&roles) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No roles found,"+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
| 	var roleKeys []string | ||||
| 	err = utils.JsonDecode(user.ChatRoles, &roleKeys) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "角色解析失败!") | ||||
| 		return | ||||
| 	} | ||||
| 	// 转成 vo | ||||
| 	var roleVos = make([]vo.ChatRole, 0) | ||||
| 	for _, r := range roles { | ||||
| 		if !utils.ContainsStr(roleKeys, r.Key) { | ||||
| 			continue | ||||
| 		} | ||||
| 		var v vo.ChatRole | ||||
| 		err := utils.CopyObject(r, &v) | ||||
| 		if err == nil { | ||||
| 			v.Id = r.Id | ||||
| 			roleVos = append(roleVos, v) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, roleVos) | ||||
| } | ||||
| @@ -1,330 +0,0 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type UserHandler struct { | ||||
| 	BaseHandler | ||||
| 	db       *gorm.DB | ||||
| 	searcher *xdb.Searcher | ||||
| } | ||||
|  | ||||
| func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher) *UserHandler { | ||||
| 	handler := &UserHandler{db: db, searcher: searcher} | ||||
| 	handler.App = app | ||||
| 	return handler | ||||
| } | ||||
|  | ||||
| // Register user register | ||||
| func (h *UserHandler) Register(c *gin.Context) { | ||||
| 	// parameters process | ||||
| 	var data struct { | ||||
| 		Username string `json:"username"` | ||||
| 		Password string `json:"password"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	data.Username = strings.TrimSpace(data.Username) | ||||
| 	data.Password = strings.TrimSpace(data.Password) | ||||
|  | ||||
| 	if len(data.Username) < 5 { | ||||
| 		resp.ERROR(c, "用户名长度不能少于5个字符") | ||||
| 		return | ||||
| 	} | ||||
| 	if len(data.Password) < 8 { | ||||
| 		resp.ERROR(c, "密码长度不能少于8个字符") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// check if the username is exists | ||||
| 	var item model.User | ||||
| 	tx := h.db.Where("username = ?", data.Username).First(&item) | ||||
| 	if tx.RowsAffected > 0 { | ||||
| 		resp.ERROR(c, "用户名已存在") | ||||
| 		return | ||||
| 	} | ||||
| 	// 默认订阅所有角色 | ||||
| 	var chatRoles []model.ChatRole | ||||
| 	h.db.Find(&chatRoles) | ||||
| 	var roleKeys = make([]string, 0) | ||||
| 	for _, r := range chatRoles { | ||||
| 		roleKeys = append(roleKeys, r.Key) | ||||
| 	} | ||||
|  | ||||
| 	salt := utils.RandString(8) | ||||
| 	user := model.User{ | ||||
| 		Username:  data.Username, | ||||
| 		Password:  utils.GenPassword(data.Password, salt), | ||||
| 		Nickname:  fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)), | ||||
| 		Avatar:    "images/avatar/user.png", | ||||
| 		Salt:      salt, | ||||
| 		Status:    true, | ||||
| 		ChatRoles: utils.JsonEncode(roleKeys), | ||||
| 		ChatConfig: utils.JsonEncode(types.ChatConfig{ | ||||
| 			Temperature:   h.App.ChatConfig.Temperature, | ||||
| 			MaxTokens:     h.App.ChatConfig.MaxTokens, | ||||
| 			EnableContext: h.App.ChatConfig.EnableContext, | ||||
| 			EnableHistory: true, | ||||
| 			Model:         h.App.ChatConfig.Model, | ||||
| 			ApiKey:        "", | ||||
| 		}), | ||||
| 	} | ||||
| 	// 初始化调用次数 | ||||
| 	var cfg model.Config | ||||
| 	h.db.Where("marker = ?", "system").First(&cfg) | ||||
| 	var config types.SystemConfig | ||||
| 	err := utils.JsonDecode(cfg.Config, &config) | ||||
| 	if err != nil || config.UserInitCalls <= 0 { | ||||
| 		user.Calls = types.UserInitCalls | ||||
| 	} else { | ||||
| 		user.Calls = config.UserInitCalls | ||||
| 	} | ||||
| 	res := h.db.Create(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "保存数据失败") | ||||
| 		logger.Error(res.Error) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, user) | ||||
| } | ||||
|  | ||||
| // Login 用户登录 | ||||
| func (h *UserHandler) Login(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Username string | ||||
| 		Password string | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	var user model.User | ||||
| 	res := h.db.Where("username = ?", data.Username).First(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "用户名不存在") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	password := utils.GenPassword(data.Password, user.Salt) | ||||
| 	if password != user.Password { | ||||
| 		resp.ERROR(c, "用户名或密码错误") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 更新最后登录时间和IP | ||||
| 	user.LastLoginIp = c.ClientIP() | ||||
| 	user.LastLoginAt = time.Now().Unix() | ||||
| 	h.db.Model(&user).Updates(user) | ||||
|  | ||||
| 	sessionId := utils.RandString(42) | ||||
| 	err := utils.SetLoginUser(c, user) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "保存会话失败") | ||||
| 		logger.Error("Error for save session: ", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 记录登录信息在服务端 | ||||
| 	h.App.ChatSession.Put(sessionId, types.ChatSession{ClientIP: c.ClientIP(), UserId: user.Id, Username: data.Username, SessionId: sessionId}) | ||||
|  | ||||
| 	// 加载用户订阅的聊天角色 | ||||
| 	var roleKeys []string | ||||
| 	err = utils.JsonDecode(user.ChatRoles, &roleKeys) | ||||
| 	var chatRoles interface{} | ||||
| 	if err == nil { | ||||
| 		var roles []model.ChatRole | ||||
| 		res = h.db.Where("marker IN ?", roleKeys).Find(&roles) | ||||
| 		if res.Error == err { | ||||
| 			type Item struct { | ||||
| 				Name string | ||||
| 				Key  string | ||||
| 				Icon string | ||||
| 			} | ||||
| 			items := make([]Item, 0) | ||||
| 			for _, r := range roles { | ||||
| 				items = append(items, Item{Name: r.Name, Key: r.Key, Icon: r.Icon}) | ||||
| 			} | ||||
| 			chatRoles = items | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	h.db.Create(&model.UserLoginLog{ | ||||
| 		UserId:       user.Id, | ||||
| 		Username:     user.Username, | ||||
| 		LoginIp:      c.ClientIP(), | ||||
| 		LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()), | ||||
| 	}) | ||||
| 	var chatConfig types.ChatConfig | ||||
| 	err = utils.JsonDecode(user.ChatConfig, &chatConfig) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, gin.H{ | ||||
| 		"session_id":     sessionId, | ||||
| 		"id":             user.Id, | ||||
| 		"nickname":       user.Nickname, | ||||
| 		"avatar":         user.Avatar, | ||||
| 		"username":       user.Username, | ||||
| 		"tokens":         user.Tokens, | ||||
| 		"calls":          user.Calls, | ||||
| 		"expiredTime":    user.ExpiredTime, | ||||
| 		"chatRoles":      chatRoles, | ||||
| 		"api_key":        chatConfig.ApiKey, | ||||
| 		"model":          chatConfig.Model, | ||||
| 		"temperature":    chatConfig.Temperature, | ||||
| 		"max_tokens":     chatConfig.MaxTokens, | ||||
| 		"enable_context": chatConfig.EnableContext, | ||||
| 		"enable_history": chatConfig.EnableHistory, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // Logout 注 销 | ||||
| func (h *UserHandler) Logout(c *gin.Context) { | ||||
| 	sessionId := c.GetHeader(types.SessionName) | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Delete(types.SessionUser) | ||||
| 	err := session.Save() | ||||
| 	if err != nil { | ||||
| 		logger.Error("Error for save session: ", err) | ||||
| 	} | ||||
| 	// 删除 websocket 会话列表 | ||||
| 	h.App.ChatSession.Delete(sessionId) | ||||
| 	// 关闭 socket 连接 | ||||
| 	client := h.App.ChatClients.Get(sessionId) | ||||
| 	if client != nil { | ||||
| 		client.Close() | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Session 获取/验证会话 | ||||
| func (h *UserHandler) Session(c *gin.Context) { | ||||
| 	sessionId := c.GetHeader(types.SessionName) | ||||
| 	session := h.App.ChatSession.Get(sessionId) | ||||
| 	if session.ClientIP == c.ClientIP() { | ||||
| 		resp.SUCCESS(c, session) | ||||
| 	} else { | ||||
| 		resp.NotAuth(c) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func (h *UserHandler) ProfileUpdate(c *gin.Context) { | ||||
| 	var data vo.User | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
| 	h.db.First(&user, user.Id) | ||||
| 	user.Nickname = data.Nickname | ||||
| 	user.Avatar = data.Avatar | ||||
|  | ||||
| 	var chatConfig types.ChatConfig | ||||
| 	err = utils.JsonDecode(user.ChatConfig, &chatConfig) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "用户配置解析失败") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	chatConfig.EnableHistory = data.ChatConfig.EnableHistory | ||||
| 	chatConfig.EnableContext = data.ChatConfig.EnableContext | ||||
| 	chatConfig.Model = data.ChatConfig.Model | ||||
| 	chatConfig.MaxTokens = data.ChatConfig.MaxTokens | ||||
| 	chatConfig.ApiKey = data.ChatConfig.ApiKey | ||||
| 	chatConfig.Temperature = data.ChatConfig.Temperature | ||||
|  | ||||
| 	user.ChatConfig = utils.JsonEncode(chatConfig) | ||||
| 	res := h.db.Updates(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新用户信息失败") | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *UserHandler) Profile(c *gin.Context) { | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	h.db.First(&user, user.Id) | ||||
| 	var userVo vo.User | ||||
| 	err = utils.CopyObject(user, &userVo) | ||||
| 	if err != nil { | ||||
| 		logger.Error("对象拷贝失败:", err.Error()) | ||||
| 		resp.ERROR(c, "获取用户信息失败") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	userVo.Id = user.Id | ||||
| 	userVo.CreatedAt = user.CreatedAt.Unix() | ||||
| 	userVo.UpdatedAt = user.UpdatedAt.Unix() | ||||
| 	resp.SUCCESS(c, userVo) | ||||
| } | ||||
|  | ||||
| // Password 更新密码 | ||||
| func (h *UserHandler) Password(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		OldPass  string `json:"old_pass"` | ||||
| 		Password string `json:"password"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(data.Password) < 8 { | ||||
| 		resp.ERROR(c, "密码长度不能少于8个字符") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	password := utils.GenPassword(data.OldPass, user.Salt) | ||||
| 	logger.Info(user.Salt, ",", user.Password, ",", password, ",", data.OldPass) | ||||
| 	if password != user.Password { | ||||
| 		resp.ERROR(c, "原密码错误") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	newPass := utils.GenPassword(data.Password, user.Salt) | ||||
| 	res := h.db.Model(&user).UpdateColumn("password", newPass) | ||||
| 	if res.Error != nil { | ||||
| 		logger.Error("更新数据库失败: ", res.Error) | ||||
| 		resp.ERROR(c, "更新数据库失败") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| package logger | ||||
|  | ||||
| import ( | ||||
| 	"go.uber.org/zap" | ||||
| ) | ||||
|  | ||||
| var logger *zap.SugaredLogger | ||||
|  | ||||
| func GetLogger() *zap.SugaredLogger { | ||||
| 	if logger != nil { | ||||
| 		return logger | ||||
| 	} | ||||
|  | ||||
| 	logLevel := zap.NewAtomicLevel() | ||||
| 	logLevel.SetLevel(zap.InfoLevel) | ||||
| 	log, _ := zap.Config{ | ||||
| 		Level:            logLevel, | ||||
| 		Development:      false, | ||||
| 		Encoding:         "console", | ||||
| 		EncoderConfig:    zap.NewDevelopmentEncoderConfig(), | ||||
| 		OutputPaths:      []string{"stderr"}, | ||||
| 		ErrorOutputPaths: []string{"stderr"}, | ||||
| 	}.Build() | ||||
| 	logger = log.Sugar() | ||||
| 	return logger | ||||
| } | ||||
							
								
								
									
										218
									
								
								api/go/main.go
									
									
									
									
									
								
							
							
						
						
									
										218
									
								
								api/go/main.go
									
									
									
									
									
								
							| @@ -1,218 +0,0 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/handler/admin" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/store" | ||||
| 	"context" | ||||
| 	"embed" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| 	"go.uber.org/fx" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
| var configFile string | ||||
| var debugMode bool | ||||
|  | ||||
| //go:embed res/ip2region.xdb | ||||
| var xdbFS embed.FS | ||||
|  | ||||
| // AppLifecycle 应用程序生命周期 | ||||
| type AppLifecycle struct { | ||||
| } | ||||
|  | ||||
| // OnStart 应用程序启动时执行 | ||||
| func (l *AppLifecycle) OnStart(context.Context) error { | ||||
| 	log.Println("AppLifecycle OnStart") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // OnStop 应用程序停止时执行 | ||||
| func (l *AppLifecycle) OnStop(context.Context) error { | ||||
| 	log.Println("AppLifecycle OnStop") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	logger.Info("Loading config file: ", configFile) | ||||
| 	app := fx.New( | ||||
| 		// 初始化配置应用配置 | ||||
| 		fx.Provide(func() *types.AppConfig { | ||||
| 			config, err := core.LoadConfig(configFile) | ||||
| 			if err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 			return config | ||||
| 		}), | ||||
| 		// 创建应用服务 | ||||
| 		fx.Provide(core.NewServer), | ||||
| 		// 初始化 | ||||
| 		fx.Invoke(func(s *core.AppServer) { | ||||
| 			s.Init(debugMode) | ||||
| 		}), | ||||
|  | ||||
| 		// 初始化数据库 | ||||
| 		fx.Provide(store.NewGormConfig), | ||||
| 		fx.Provide(store.NewMysql), | ||||
| 		fx.Provide(store.NewLevelDB), | ||||
|  | ||||
| 		// 创建 Ip2Region 查询对象 | ||||
| 		fx.Provide(func() (*xdb.Searcher, error) { | ||||
| 			file, err := xdbFS.Open("res/ip2region.xdb") | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			cBuff, err := io.ReadAll(file) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
|  | ||||
| 			return xdb.NewWithBuffer(cBuff) | ||||
| 		}), | ||||
|  | ||||
| 		// 创建控制器 | ||||
| 		fx.Provide(handler.NewChatRoleHandler), | ||||
| 		fx.Provide(handler.NewUserHandler), | ||||
| 		fx.Provide(handler.NewChatHandler), | ||||
| 		fx.Provide(admin.NewConfigHandler), | ||||
|  | ||||
| 		fx.Provide(admin.NewAdminHandler), | ||||
| 		fx.Provide(admin.NewApiKeyHandler), | ||||
| 		fx.Provide(admin.NewUserHandler), | ||||
| 		fx.Provide(admin.NewChatRoleHandler), | ||||
|  | ||||
| 		// 注册路由 | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) { | ||||
| 			group := s.Engine.Group("/api/role/") | ||||
| 			group.GET("list", h.List) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) { | ||||
| 			group := s.Engine.Group("/api/user/") | ||||
| 			group.POST("register", h.Register) | ||||
| 			group.POST("login", h.Login) | ||||
| 			group.GET("logout", h.Logout) | ||||
| 			group.GET("session", h.Session) | ||||
| 			group.GET("profile", h.Profile) | ||||
| 			group.POST("profile/update", h.ProfileUpdate) | ||||
| 			group.POST("password", h.Password) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) { | ||||
| 			group := s.Engine.Group("/api/chat/") | ||||
| 			group.Any("new", h.ChatHandle) | ||||
| 			group.GET("list", h.List) | ||||
| 			group.POST("update", h.Update) | ||||
| 			group.GET("remove", h.Remove) | ||||
| 			group.GET("history", h.History) | ||||
| 			group.GET("clear", h.Clear) | ||||
| 			group.GET("tokens", h.Tokens) | ||||
| 			group.GET("stop", h.StopGenerate) | ||||
| 		}), | ||||
|  | ||||
| 		// | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/config/") | ||||
| 			group.POST("update", h.Update) | ||||
| 			group.GET("get", h.Get) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/") | ||||
| 			group.POST("login", h.Login) | ||||
| 			group.GET("logout", h.Logout) | ||||
| 			group.GET("session", h.Session) | ||||
| 			group.GET("migrate", h.Migrate) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/apikey/") | ||||
| 			group.POST("save", h.Save) | ||||
| 			group.GET("list", h.List) | ||||
| 			group.GET("remove", h.Remove) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/user/") | ||||
| 			group.GET("list", h.List) | ||||
| 			group.POST("update", h.Update) | ||||
| 			group.GET("remove", h.Remove) | ||||
| 			group.GET("loginLog", h.LoginLog) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/role/") | ||||
| 			group.GET("list", h.List) | ||||
| 			group.POST("save", h.Save) | ||||
| 			group.POST("sort", h.SetSort) | ||||
| 			group.GET("remove", h.Remove) | ||||
| 		}), | ||||
|  | ||||
| 		fx.Invoke(func(s *core.AppServer, db *gorm.DB) { | ||||
| 			err := s.Run(db) | ||||
| 			if err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 		}), | ||||
|  | ||||
| 		// 注册生命周期回调函数 | ||||
| 		fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) { | ||||
| 			lifecycle.Append(fx.Hook{ | ||||
| 				OnStart: func(ctx context.Context) error { | ||||
| 					return lc.OnStart(ctx) | ||||
| 				}, | ||||
| 				OnStop: func(ctx context.Context) error { | ||||
| 					return lc.OnStop(ctx) | ||||
| 				}, | ||||
| 			}) | ||||
| 		}), | ||||
| 	) | ||||
| 	// 启动应用程序 | ||||
| 	go func() { | ||||
| 		if err := app.Start(context.Background()); err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	// 监听退出信号 | ||||
| 	quit := make(chan os.Signal, 1) | ||||
| 	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) | ||||
| 	<-quit | ||||
|  | ||||
| 	// 关闭应用程序 | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||||
| 	defer cancel() | ||||
| 	if err := app.Stop(ctx); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	flag.StringVar(&configFile, "config", "config.toml", "AppConfig file path (default: config.toml)") | ||||
| 	flag.BoolVar(&debugMode, "debug", true, "Enable debug mode (default: true, recommend to set false in production env)") | ||||
| 	flag.Usage = usage | ||||
| 	flag.Parse() | ||||
| } | ||||
|  | ||||
| func usage() { | ||||
| 	fmt.Printf(`ChatGPT-Web-Plus, Version: 2.0.0 | ||||
| USAGE:  | ||||
|   %s [command options] | ||||
| OPTIONS: | ||||
| `, os.Args[0]) | ||||
|  | ||||
| 	flagSet := flag.CommandLine | ||||
| 	order := []string{"config", "debug"} | ||||
| 	for _, name := range order { | ||||
| 		f := flagSet.Lookup(name) | ||||
| 		fmt.Printf("  --%s => %s\n", f.Name, f.Usage) | ||||
| 	} | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| package model | ||||
|  | ||||
| // ApiKey OpenAI API 模型 | ||||
| type ApiKey struct { | ||||
| 	BaseModel | ||||
| 	UserId     uint   //用户ID,系统添加的用户 ID 为 0 | ||||
| 	Value      string // API Key 的值 | ||||
| 	LastUsedAt int64  // 最后使用时间 | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| package model | ||||
|  | ||||
| type HistoryMessage struct { | ||||
| 	BaseModel | ||||
| 	ChatId  string // 会话 ID | ||||
| 	UserId  uint   // 用户 ID | ||||
| 	RoleId  uint   // 角色 ID | ||||
| 	Type    string | ||||
| 	Icon    string | ||||
| 	Tokens  int | ||||
| 	Content string | ||||
| } | ||||
|  | ||||
| func (HistoryMessage) TableName() string { | ||||
| 	return "chatgpt_chat_history" | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package model | ||||
|  | ||||
| type ChatItem struct { | ||||
| 	BaseModel | ||||
| 	ChatId string `gorm:"column:chat_id;unique"` // 会话 ID | ||||
| 	UserId uint   // 用户 ID | ||||
| 	RoleId uint   // 角色 ID | ||||
| 	Model  string // 会话模型 | ||||
| 	Title  string // 会话标题 | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| package vo | ||||
|  | ||||
| // ApiKey OpenAI API 模型 | ||||
| type ApiKey struct { | ||||
| 	BaseVo | ||||
| 	UserId     uint   `json:"user_id"`      //用户ID,系统添加的用户 ID 为 0 | ||||
| 	Value      string `json:"value"`        // API Key 的值 | ||||
| 	LastUsedAt int64  `json:"last_used_at"` // 最后使用时间 | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| package vo | ||||
|  | ||||
| type HistoryMessage struct { | ||||
| 	BaseVo | ||||
| 	ChatId  string `json:"chat_id"` | ||||
| 	UserId  uint   `json:"user_id"` | ||||
| 	RoleId  uint   `json:"role_id"` | ||||
| 	Type    string `json:"type"` | ||||
| 	Icon    string `json:"icon"` | ||||
| 	Tokens  int    `json:"tokens"` | ||||
| 	Content string `json:"content"` | ||||
| } | ||||
|  | ||||
| func (HistoryMessage) TableName() string { | ||||
| 	return "chatgpt_chat_history" | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package vo | ||||
|  | ||||
| type ChatItem struct { | ||||
| 	BaseVo | ||||
| 	UserId uint   `json:"user_id"` | ||||
| 	Icon   string `json:"icon"` | ||||
| 	RoleId uint   `json:"role_id"` | ||||
| 	ChatId string `json:"chat_id"` | ||||
| 	Model  string `json:"model"` | ||||
| 	Title  string `json:"title"` | ||||
| } | ||||
| @@ -1,14 +0,0 @@ | ||||
| package vo | ||||
|  | ||||
| import "chatplus/core/types" | ||||
|  | ||||
| type ChatRole struct { | ||||
| 	BaseVo | ||||
| 	Key      string          `json:"key"`       // 角色唯一标识 | ||||
| 	Name     string          `json:"name"`      // 角色名称 | ||||
| 	Context  []types.Message `json:"context"`   // 角色语料信息 | ||||
| 	HelloMsg string          `json:"hello_msg"` // 打招呼的消息 | ||||
| 	Icon     string          `json:"icon"`      // 角色聊天图标 | ||||
| 	Enable   bool            `json:"enable"`    // 是否启用被启用 | ||||
| 	Sort     int             `json:"sort"`      // 排序 | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| package vo | ||||
|  | ||||
| import "chatplus/core/types" | ||||
|  | ||||
| type User struct { | ||||
| 	BaseVo | ||||
| 	Username    string           `json:"username"` | ||||
| 	Nickname    string           `json:"nickname"` | ||||
| 	Avatar      string           `json:"avatar"` | ||||
| 	Salt        string           `json:"salt"`          // 密码盐 | ||||
| 	Tokens      int64            `json:"tokens"`        // 剩余tokens | ||||
| 	Calls       int              `json:"calls"`         // 剩余对话次数 | ||||
| 	ChatConfig  types.ChatConfig `json:"chat_config"`   // 聊天配置 | ||||
| 	ChatRoles   []string         `json:"chat_roles"`    // 聊天角色集合 | ||||
| 	ExpiredTime int64            `json:"expired_time"`  // 账户到期时间 | ||||
| 	Status      bool             `json:"status"`        // 当前状态 | ||||
| 	LastLoginAt int64            `json:"last_login_at"` // 最后登录时间 | ||||
| 	LastLoginIp string           `json:"last_login_ip"` // 最后登录 IP | ||||
| } | ||||
| @@ -1,145 +0,0 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| 	"github.com/pkoukk/tiktoken-go" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	lMap := types.NewLMap[string, types.ChatSession]() | ||||
| 	lMap.Put("name", types.ChatSession{SessionId: utils.RandString(32)}) | ||||
|  | ||||
| 	item := lMap.Get("abc") | ||||
| 	fmt.Println(item) | ||||
| } | ||||
|  | ||||
| // Http client 取消操作 | ||||
| func testHttpClient(ctx context.Context) { | ||||
|  | ||||
| 	req, err := http.NewRequest("GET", "http://localhost:2345", nil) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	req = req.WithContext(ctx) | ||||
|  | ||||
| 	client := &http.Client{} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer func(Body io.ReadCloser) { | ||||
| 		err := Body.Close() | ||||
| 		if err != nil { | ||||
|  | ||||
| 		} | ||||
| 	}(resp.Body) | ||||
| 	_, err = io.ReadAll(resp.Body) | ||||
| 	for { | ||||
| 		time.Sleep(time.Second) | ||||
| 		fmt.Println(time.Now()) | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			fmt.Println("取消退出") | ||||
| 			return | ||||
| 		default: | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func testDate() { | ||||
| 	fmt.Println(time.Unix(1683336167, 0).Format("2006-01-02 15:04:05")) | ||||
| } | ||||
|  | ||||
| func testIp2Region() { | ||||
| 	dbPath := "res/ip2region.xdb" | ||||
| 	// 1、从 dbPath 加载整个 xdb 到内存 | ||||
| 	cBuff, err := xdb.LoadContentFromFile(dbPath) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 2、用全局的 cBuff 创建完全基于内存的查询对象。 | ||||
| 	searcher, err := xdb.NewWithBuffer(cBuff) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("failed to create searcher with content: %s\n", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	str, err := searcher.SearchByStr("103.88.46.85") | ||||
| 	fmt.Println(str) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	arr := strings.Split(str, "|") | ||||
| 	fmt.Println(arr[2], arr[3], arr[4]) | ||||
|  | ||||
| } | ||||
|  | ||||
| func testJson() { | ||||
|  | ||||
| 	var role = model.ChatRole{ | ||||
| 		Key:      "programmer", | ||||
| 		Name:     "程序员", | ||||
| 		Context:  "[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\"\n:\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]", | ||||
| 		HelloMsg: "Talk is cheap, i will show code!", | ||||
| 		Icon:     "images/avatar/programmer.jpg", | ||||
| 		Enable:   true, | ||||
| 		Sort:     1, | ||||
| 	} | ||||
| 	role.Id = 1 | ||||
| 	var v vo.ChatRole | ||||
|  | ||||
| 	err := utils.CopyObject(role, &v) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("%+v\n", v.Id) | ||||
|  | ||||
| 	//var v2 = model.ChatRoles{} | ||||
| 	//err = utils.CopyObject(v, &v2) | ||||
| 	//if err != nil { | ||||
| 	//	log.Fatal(err) | ||||
| 	//} | ||||
| 	// | ||||
| 	//fmt.Printf("%+v\n", v2.Id) | ||||
|  | ||||
| } | ||||
|  | ||||
| func calTokens() { | ||||
| 	text := "须知少年凌云志,曾许人间第一流" | ||||
| 	encoding := "cl100k_base" | ||||
|  | ||||
| 	tke, err := tiktoken.GetEncoding(encoding) | ||||
| 	if err != nil { | ||||
| 		err = fmt.Errorf("getEncoding: %v", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// encode | ||||
| 	token := tke.Encode(text, nil, nil) | ||||
|  | ||||
| 	//tokens | ||||
| 	fmt.Println(token) | ||||
| 	// num_tokens | ||||
| 	fmt.Println(len(token)) | ||||
|  | ||||
| } | ||||
| @@ -1,89 +0,0 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| ) | ||||
|  | ||||
| // CopyObject 拷贝对象 | ||||
| func CopyObject(src interface{}, dst interface{}) error { | ||||
|  | ||||
| 	srcType := reflect.TypeOf(src) | ||||
| 	srcValue := reflect.ValueOf(src) | ||||
| 	dstValue := reflect.ValueOf(dst).Elem() | ||||
| 	reflect.TypeOf(dst) | ||||
| 	for i := 0; i < srcType.NumField(); i++ { | ||||
| 		field := srcType.Field(i) | ||||
| 		value := dstValue.FieldByName(field.Name) | ||||
| 		if !value.IsValid() { | ||||
| 			continue | ||||
| 		} | ||||
| 		// 数据类型相同,直接赋值 | ||||
| 		v := srcValue.FieldByName(field.Name) | ||||
| 		if value.Type() == field.Type { | ||||
| 			value.Set(v) | ||||
| 		} else { | ||||
| 			// src data type is  string,dst data type is slice, map, struct | ||||
| 			// use json decode the data | ||||
| 			if field.Type.Kind() == reflect.String && (value.Type().Kind() == reflect.Struct || | ||||
| 				value.Type().Kind() == reflect.Map || | ||||
| 				value.Type().Kind() == reflect.Slice) { | ||||
| 				pType := reflect.New(value.Type()) | ||||
| 				v2 := pType.Interface() | ||||
| 				err := json.Unmarshal([]byte(v.String()), &v2) | ||||
| 				if err == nil { | ||||
| 					value.Set(reflect.ValueOf(v2).Elem()) | ||||
| 				} | ||||
| 				// map, struct, slice to string | ||||
| 			} else if (field.Type.Kind() == reflect.Struct || | ||||
| 				field.Type.Kind() == reflect.Map || | ||||
| 				field.Type.Kind() == reflect.Slice) && value.Type().Kind() == reflect.String { | ||||
| 				ba, err := json.Marshal(v.Interface()) | ||||
| 				if err == nil { | ||||
| 					val := string(ba) | ||||
| 					if strings.Contains(val, "{") { | ||||
| 						value.Set(reflect.ValueOf(string(ba))) | ||||
| 					} else { | ||||
| 						value.Set(reflect.ValueOf("")) | ||||
| 					} | ||||
| 				} | ||||
| 			} else { // 简单数据类型的强制类型转换 | ||||
| 				switch value.Kind() { | ||||
| 				case reflect.Int: | ||||
| 				case reflect.Int8: | ||||
| 				case reflect.Int16: | ||||
| 				case reflect.Int32: | ||||
| 				case reflect.Int64: | ||||
| 					value.SetInt(v.Int()) | ||||
| 					break | ||||
| 				case reflect.Float32: | ||||
| 				case reflect.Float64: | ||||
| 					value.SetFloat(v.Float()) | ||||
| 					break | ||||
| 				case reflect.Bool: | ||||
| 					value.SetBool(v.Bool()) | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func Ip2Region(searcher *xdb.Searcher, ip string) string { | ||||
| 	str, err := searcher.SearchByStr(ip) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	arr := strings.Split(str, "|") | ||||
| 	if len(arr) < 3 { | ||||
| 		return arr[0] | ||||
| 	} | ||||
| 	return fmt.Sprintf("%s-%s-%s", arr[0], arr[2], arr[3]) | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/pkoukk/tiktoken-go" | ||||
| ) | ||||
|  | ||||
| func CalcTokens(text string, model string) (int, error) { | ||||
| 	encoding := tiktoken.MODEL_TO_ENCODING[model] | ||||
| 	tke, err := tiktoken.GetEncoding(encoding) | ||||
| 	if err != nil { | ||||
| 		return 0, fmt.Errorf("getEncoding: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	token := tke.Encode(text, nil, nil) | ||||
| 	return len(token), nil | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| package resp | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| func SUCCESS(c *gin.Context, values ...interface{}) { | ||||
| 	if values != nil { | ||||
| 		c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: values[0]}) | ||||
| 	} else { | ||||
| 		c.JSON(http.StatusOK, types.BizVo{Code: types.Success}) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func ERROR(c *gin.Context, messages ...string) { | ||||
| 	if messages != nil { | ||||
| 		c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: messages[0]}) | ||||
| 	} else { | ||||
| 		c.JSON(http.StatusOK, types.BizVo{Code: types.Failed}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func HACKER(c *gin.Context) { | ||||
| 	c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Hacker attempt!!!"}) | ||||
| } | ||||
|  | ||||
| func NotAuth(c *gin.Context) { | ||||
| 	c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: "Not Authorized"}) | ||||
| } | ||||
| @@ -1,79 +0,0 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"time" | ||||
|  | ||||
| 	"golang.org/x/crypto/sha3" | ||||
| ) | ||||
|  | ||||
| // RandString generate rand string with specified length | ||||
| func RandString(length int) string { | ||||
| 	str := "0123456789abcdefghijklmnopqrstuvwxyz" | ||||
| 	data := []byte(str) | ||||
| 	var result []byte | ||||
| 	r := rand.New(rand.NewSource(time.Now().UnixNano())) | ||||
| 	for i := 0; i < length; i++ { | ||||
| 		result = append(result, data[r.Intn(len(data))]) | ||||
| 	} | ||||
| 	return string(result) | ||||
| } | ||||
|  | ||||
| func RandomNumber(bit int) int { | ||||
| 	rand.Seed(time.Now().UnixNano()) | ||||
| 	min := 1 // min value | ||||
| 	max := 1 //max value | ||||
| 	for i := 0; i < bit; i++ { | ||||
| 		min = min * 10 | ||||
| 		max = max * 10 | ||||
| 	} | ||||
| 	max = max * 10 | ||||
| 	return rand.Intn(max-min+1) + min | ||||
| } | ||||
|  | ||||
| func ContainsStr(slice []string, item string) bool { | ||||
| 	for _, e := range slice { | ||||
| 		if e == item { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Stamp2str 时间戳转字符串 | ||||
| func Stamp2str(timestamp int64) string { | ||||
| 	if timestamp == 0 { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05") | ||||
| } | ||||
|  | ||||
| // Str2stamp 字符串转时间戳 | ||||
| func Str2stamp(str string) int64 { | ||||
| 	layout := "2006-01-02 15:04:05" | ||||
| 	t, err := time.Parse(layout, str) | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return t.Unix() | ||||
| } | ||||
|  | ||||
| func GenPassword(pass string, salt string) string { | ||||
| 	data := []byte(pass + salt) | ||||
| 	hash := sha3.Sum256(data) | ||||
| 	return fmt.Sprintf("%x", hash) | ||||
| } | ||||
|  | ||||
| func JsonEncode(value interface{}) string { | ||||
| 	bytes, err := json.Marshal(value) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return string(bytes) | ||||
| } | ||||
|  | ||||
| func JsonDecode(src string, dest interface{}) error { | ||||
| 	return json.Unmarshal([]byte(src), dest) | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| func SetLoginUser(c *gin.Context, user model.User) error { | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Set(types.SessionUser, user.Id) | ||||
| 	// TODO: 后期用户数量增加,考虑将用户数据存储到 leveldb,避免每次查询数据库 | ||||
| 	return session.Save() | ||||
| } | ||||
|  | ||||
| func SetLoginAdmin(c *gin.Context, admin types.Manager) error { | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Set(types.SessionAdmin, admin.Username) | ||||
| 	return session.Save() | ||||
| } | ||||
|  | ||||
| func GetLoginUser(c *gin.Context, db *gorm.DB) (model.User, error) { | ||||
| 	value, exists := c.Get(types.LoginUserCache) | ||||
| 	if exists { | ||||
| 		return value.(model.User), nil | ||||
| 	} | ||||
|  | ||||
| 	session := sessions.Default(c) | ||||
| 	userId := session.Get(types.SessionUser) | ||||
| 	if userId == nil { | ||||
| 		return model.User{}, errors.New("user not login") | ||||
| 	} | ||||
|  | ||||
| 	var user model.User | ||||
| 	res := db.First(&user, userId) | ||||
| 	// 更新缓存 | ||||
| 	if res.Error == nil { | ||||
| 		c.Set(types.LoginUserCache, user) | ||||
| 	} | ||||
| 	return user, res.Error | ||||
| } | ||||
							
								
								
									
										289
									
								
								api/handler/admin/admin_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								api/handler/admin/admin_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,289 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	logger2 "geekai/logger" | ||||
| 	"geekai/service" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| const SuperManagerID = 1 | ||||
|  | ||||
| type ManagerHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	redis   *redis.Client | ||||
| 	captcha *service.CaptchaService | ||||
| } | ||||
|  | ||||
| func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client, captcha *service.CaptchaService) *ManagerHandler { | ||||
| 	return &ManagerHandler{ | ||||
| 		BaseHandler: handler.BaseHandler{DB: db, App: app}, | ||||
| 		redis:       client, | ||||
| 		captcha:     captcha, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Login 登录 | ||||
| func (h *ManagerHandler) Login(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Username string `json:"username"` | ||||
| 		Password string `json:"password"` | ||||
| 		Key      string `json:"key,omitempty"` | ||||
| 		Dots     string `json:"dots,omitempty"` | ||||
| 		X        int    `json:"x,omitempty"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if h.App.SysConfig.EnabledVerify { | ||||
| 		var check bool | ||||
| 		if data.X != 0 { | ||||
| 			check = h.captcha.SlideCheck(data) | ||||
| 		} else { | ||||
| 			check = h.captcha.Check(data) | ||||
| 		} | ||||
| 		if !check { | ||||
| 			resp.ERROR(c, "请先完人机验证") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var manager model.AdminUser | ||||
| 	res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "请检查用户名或者密码是否填写正确") | ||||
| 		return | ||||
| 	} | ||||
| 	password := utils.GenPassword(data.Password, manager.Salt) | ||||
| 	if password != manager.Password { | ||||
| 		resp.ERROR(c, "用户名或密码错误") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 超级管理员默认是ID:1 | ||||
| 	if manager.Id != SuperManagerID && manager.Status == false { | ||||
| 		resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 创建 token | ||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | ||||
| 		"user_id": manager.Id, | ||||
| 		"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(), | ||||
| 	}) | ||||
| 	tokenString, err := token.SignedString([]byte(h.App.Config.AdminSession.SecretKey)) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "Failed to generate token, "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	// 保存到 redis | ||||
| 	key := fmt.Sprintf("admin/%d", manager.Id) | ||||
| 	if _, err := h.redis.Set(context.Background(), key, tokenString, 0).Result(); err != nil { | ||||
| 		resp.ERROR(c, "error with save token: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 更新最后登录时间和IP | ||||
| 	manager.LastLoginIp = c.ClientIP() | ||||
| 	manager.LastLoginAt = time.Now().Unix() | ||||
| 	h.DB.Updates(&manager) | ||||
|  | ||||
| 	var result = struct { | ||||
| 		IsSuperAdmin bool   `json:"is_super_admin"` | ||||
| 		Token        string `json:"token"` | ||||
| 	}{ | ||||
| 		IsSuperAdmin: manager.Id == 1, | ||||
| 		Token:        tokenString, | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, result) | ||||
| } | ||||
|  | ||||
| // Logout 注销 | ||||
| func (h *ManagerHandler) Logout(c *gin.Context) { | ||||
| 	key := h.GetUserKey(c) | ||||
| 	if _, err := h.redis.Del(c, key).Result(); err != nil { | ||||
| 		logger.Error("error with delete session: ", err) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Session 会话检测 | ||||
| func (h *ManagerHandler) Session(c *gin.Context) { | ||||
| 	id := h.GetLoginUserId(c) | ||||
| 	key := fmt.Sprintf("admin/%d", id) | ||||
| 	if _, err := h.redis.Get(context.Background(), key).Result(); err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
| 	var manager model.AdminUser | ||||
| 	res := h.DB.Where("id", id).First(&manager) | ||||
| 	if res.Error != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, manager) | ||||
| } | ||||
|  | ||||
| // List 数据列表 | ||||
| func (h *ManagerHandler) List(c *gin.Context) { | ||||
| 	var items []model.AdminUser | ||||
| 	res := h.DB.Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	users := make([]vo.AdminUser, 0) | ||||
| 	for _, item := range items { | ||||
| 		var u vo.AdminUser | ||||
| 		err := utils.CopyObject(item, &u) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		u.Id = item.Id | ||||
| 		u.CreatedAt = item.CreatedAt.Unix() | ||||
| 		users = append(users, u) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, users) | ||||
|  | ||||
| } | ||||
|  | ||||
| func (h *ManagerHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Username string `json:"username"` | ||||
| 		Password string `json:"password"` | ||||
| 		Status   bool   `json:"status"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var user model.AdminUser | ||||
| 	res := h.DB.Where("username", data.Username).First(&user) | ||||
| 	if res.Error == nil { | ||||
| 		resp.ERROR(c, "用户名已存在") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 生成密码 | ||||
| 	salt := utils.RandString(8) | ||||
| 	password := utils.GenPassword(data.Password, salt) | ||||
| 	res = h.DB.Save(&model.AdminUser{ | ||||
| 		Username: data.Username, | ||||
| 		Password: password, | ||||
| 		Salt:     salt, | ||||
| 		Status:   data.Status, | ||||
| 	}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "failed with update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Remove 删除管理员 | ||||
| func (h *ManagerHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	if id <= 0 { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if id == SuperManagerID { | ||||
| 		resp.ERROR(c, "超级管理员不能删除") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res := h.DB.Where("id", id).Delete(&model.AdminUser{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Enable 启用/禁用 | ||||
| func (h *ManagerHandler) Enable(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id      uint `json:"id"` | ||||
| 		Enabled bool `json:"enabled"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res := h.DB.Model(&model.AdminUser{}).Where("id", data.Id).UpdateColumn("status", data.Enabled) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // ResetPass 重置密码 | ||||
| func (h *ManagerHandler) ResetPass(c *gin.Context) { | ||||
| 	id := h.GetLoginUserId(c) | ||||
| 	if id != SuperManagerID { | ||||
| 		resp.ERROR(c, "只有超级管理员能够进行该操作") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var data struct { | ||||
| 		Id       int    `json:"id"` | ||||
| 		Password string `json:"password"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var user model.AdminUser | ||||
| 	res := h.DB.Where("id", data.Id).First(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	password := utils.GenPassword(data.Password, user.Salt) | ||||
| 	user.Password = password | ||||
| 	res = h.DB.Updates(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										139
									
								
								api/handler/admin/api_key_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								api/handler/admin/api_key_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ApiKeyHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler { | ||||
| 	return &ApiKeyHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}} | ||||
| } | ||||
|  | ||||
| func (h *ApiKeyHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id       uint   `json:"id"` | ||||
| 		Name     string `json:"name"` | ||||
| 		Type     string `json:"type"` | ||||
| 		Value    string `json:"value"` | ||||
| 		ApiURL   string `json:"api_url"` | ||||
| 		Enabled  bool   `json:"enabled"` | ||||
| 		ProxyURL string `json:"proxy_url"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	apiKey := model.ApiKey{} | ||||
| 	if data.Id > 0 { | ||||
| 		h.DB.Find(&apiKey, data.Id) | ||||
| 	} | ||||
| 	apiKey.Value = data.Value | ||||
| 	apiKey.Type = data.Type | ||||
| 	apiKey.ApiURL = data.ApiURL | ||||
| 	apiKey.Enabled = data.Enabled | ||||
| 	apiKey.ProxyURL = data.ProxyURL | ||||
| 	apiKey.Name = data.Name | ||||
| 	err := h.DB.Save(&apiKey).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var keyVo vo.ApiKey | ||||
| 	err = utils.CopyObject(apiKey, &keyVo) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, fmt.Sprintf("拷贝数据失败:%v", err)) | ||||
| 		return | ||||
| 	} | ||||
| 	keyVo.Id = apiKey.Id | ||||
| 	keyVo.CreatedAt = apiKey.CreatedAt.Unix() | ||||
| 	resp.SUCCESS(c, keyVo) | ||||
| } | ||||
|  | ||||
| func (h *ApiKeyHandler) List(c *gin.Context) { | ||||
| 	status := h.GetBool(c, "status") | ||||
| 	t := h.GetTrim(c, "type") | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if status { | ||||
| 		session = session.Where("enabled", true) | ||||
| 	} | ||||
| 	if t != "" { | ||||
| 		session = session.Where("type", t) | ||||
| 	} | ||||
|  | ||||
| 	var items []model.ApiKey | ||||
| 	var keys = make([]vo.ApiKey, 0) | ||||
| 	res := session.Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var key vo.ApiKey | ||||
| 			err := utils.CopyObject(item, &key) | ||||
| 			if err == nil { | ||||
| 				key.Id = item.Id | ||||
| 				key.CreatedAt = item.CreatedAt.Unix() | ||||
| 				key.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				keys = append(keys, key) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, keys) | ||||
| } | ||||
|  | ||||
| func (h *ApiKeyHandler) Set(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id    uint        `json:"id"` | ||||
| 		Filed string      `json:"filed"` | ||||
| 		Value interface{} `json:"value"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Model(&model.ApiKey{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ApiKeyHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	if id <= 0 { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Where("id", id).Delete(&model.ApiKey{}).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										182
									
								
								api/handler/admin/chat_app_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								api/handler/admin/chat_app_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatAppHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler { | ||||
| 	return &ChatAppHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| // Save 创建或者更新某个角色 | ||||
| func (h *ChatAppHandler) Save(c *gin.Context) { | ||||
| 	var data vo.ChatRole | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	var role model.ChatRole | ||||
| 	err := utils.CopyObject(data, &role) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	role.Id = data.Id | ||||
| 	if data.CreatedAt > 0 { | ||||
| 		role.CreatedAt = time.Unix(data.CreatedAt, 0) | ||||
| 	} else { | ||||
| 		err = h.DB.Where("marker", data.Key).First(&role).Error | ||||
| 		if err == nil { | ||||
| 			resp.ERROR(c, fmt.Sprintf("角色 %s 已存在", data.Key)) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	err = h.DB.Save(&role).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	// 填充 ID 数据 | ||||
| 	data.Id = role.Id | ||||
| 	data.CreatedAt = role.CreatedAt.Unix() | ||||
| 	resp.SUCCESS(c, data) | ||||
| } | ||||
|  | ||||
| func (h *ChatAppHandler) List(c *gin.Context) { | ||||
| 	var items []model.ChatRole | ||||
| 	var roles = make([]vo.ChatRole, 0) | ||||
| 	res := h.DB.Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No data found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// initialize model mane for role | ||||
| 	modelIds := make([]int, 0) | ||||
| 	typeIds := make([]int, 0) | ||||
| 	for _, v := range items { | ||||
| 		if v.ModelId > 0 { | ||||
| 			modelIds = append(modelIds, v.ModelId) | ||||
| 		} | ||||
| 		if v.Tid > 0 { | ||||
| 			typeIds = append(typeIds, v.Tid) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	modelNameMap := make(map[int]string) | ||||
| 	typeNameMap := make(map[int]string) | ||||
| 	if len(modelIds) > 0 { | ||||
| 		var models []model.ChatModel | ||||
| 		tx := h.DB.Where("id IN ?", modelIds).Find(&models) | ||||
| 		if tx.Error == nil { | ||||
| 			for _, m := range models { | ||||
| 				modelNameMap[int(m.Id)] = m.Name | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if len(typeIds) > 0 { | ||||
| 		var appTypes []model.AppType | ||||
| 		tx := h.DB.Where("id IN ?", typeIds).Find(&appTypes) | ||||
| 		if tx.Error == nil { | ||||
| 			for _, m := range appTypes { | ||||
| 				typeNameMap[int(m.Id)] = m.Name | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range items { | ||||
| 		var role vo.ChatRole | ||||
| 		err := utils.CopyObject(v, &role) | ||||
| 		if err == nil { | ||||
| 			role.Id = v.Id | ||||
| 			role.CreatedAt = v.CreatedAt.Unix() | ||||
| 			role.UpdatedAt = v.UpdatedAt.Unix() | ||||
| 			role.ModelName = modelNameMap[role.ModelId] | ||||
| 			role.TypeName = typeNameMap[role.Tid] | ||||
| 			roles = append(roles, role) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, roles) | ||||
| } | ||||
|  | ||||
| // Sort 更新角色排序 | ||||
| func (h *ChatAppHandler) Sort(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Ids   []uint `json:"ids"` | ||||
| 		Sorts []int  `json:"sorts"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for index, id := range data.Ids { | ||||
| 		err := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ChatAppHandler) Set(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id    uint        `json:"id"` | ||||
| 		Filed string      `json:"filed"` | ||||
| 		Value interface{} `json:"value"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ChatAppHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
|  | ||||
| 	if id <= 0 { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	res := h.DB.Where("id", id).Delete(&model.ChatRole{}) | ||||
| 	if res.Error != nil { | ||||
| 		logger.Error("error with update database:", res.Error) | ||||
| 		resp.ERROR(c, "删除失败!") | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										148
									
								
								api/handler/admin/chat_app_type_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								api/handler/admin/chat_app_type_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatAppTypeHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler { | ||||
| 	return &ChatAppTypeHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| // Save 创建或更新App类型 | ||||
| func (h *ChatAppTypeHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id      uint   `json:"id"` | ||||
| 		Name    string `json:"name"` | ||||
| 		Enabled bool   `json:"enabled"` | ||||
| 		Icon    string `json:"icon"` | ||||
| 		SortNum int    `json:"sort_num"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if data.Id == 0 { // for add | ||||
| 		err := h.DB.Where("name", data.Name).First(&model.AppType{}).Error | ||||
| 		if err == nil { | ||||
| 			resp.ERROR(c, "当前分类已经存在") | ||||
| 			return | ||||
| 		} | ||||
| 		err = h.DB.Create(&model.AppType{ | ||||
| 			Name:    data.Name, | ||||
| 			Icon:    data.Icon, | ||||
| 			Enabled: data.Enabled, | ||||
| 			SortNum: data.SortNum, | ||||
| 		}).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} else { // for update | ||||
| 		err := h.DB.Model(&model.AppType{}).Where("id", data.Id).Updates(map[string]interface{}{ | ||||
| 			"name":    data.Name, | ||||
| 			"icon":    data.Icon, | ||||
| 			"enabled": data.Enabled, | ||||
| 		}).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // List 获取App类型列表 | ||||
| func (h *ChatAppTypeHandler) List(c *gin.Context) { | ||||
| 	var items []model.AppType | ||||
| 	var appTypes = make([]vo.AppType, 0) | ||||
| 	err := h.DB.Order("sort_num ASC").Find(&items).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range items { | ||||
| 		var appType vo.AppType | ||||
| 		err = utils.CopyObject(v, &appType) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		appType.Id = v.Id | ||||
| 		appType.CreatedAt = v.CreatedAt.Unix() | ||||
| 		appTypes = append(appTypes, appType) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, appTypes) | ||||
| } | ||||
|  | ||||
| // Remove 删除App类型 | ||||
| func (h *ChatAppTypeHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
|  | ||||
| 	if id <= 0 { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	err := h.DB.Where("id", id).Delete(&model.AppType{}).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Enable 启用|禁用 | ||||
| func (h *ChatAppTypeHandler) Enable(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id      uint `json:"id"` | ||||
| 		Enabled bool `json:"enabled"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Model(&model.AppType{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Sort 更新排序 | ||||
| func (h *ChatAppTypeHandler) Sort(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Ids   []uint `json:"ids"` | ||||
| 		Sorts []int  `json:"sorts"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for index, id := range data.Ids { | ||||
| 		err := h.DB.Model(&model.AppType{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										268
									
								
								api/handler/admin/chat_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								api/handler/admin/chat_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler { | ||||
| 	return &ChatHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| type chatItemVo struct { | ||||
| 	Username  string      `json:"username"` | ||||
| 	UserId    uint        `json:"user_id"` | ||||
| 	ChatId    string      `json:"chat_id"` | ||||
| 	Title     string      `json:"title"` | ||||
| 	Role      vo.ChatRole `json:"role"` | ||||
| 	Model     string      `json:"model"` | ||||
| 	Token     int         `json:"token"` | ||||
| 	CreatedAt int64       `json:"created_at"` | ||||
| 	MsgNum    int         `json:"msg_num"` // 消息数量 | ||||
| } | ||||
|  | ||||
| func (h *ChatHandler) List(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Title    string   `json:"title"` | ||||
| 		UserId   uint     `json:"user_id"` | ||||
| 		Model    string   `json:"model"` | ||||
| 		CreateAt []string `json:"created_time"` | ||||
| 		Page     int      `json:"page"` | ||||
| 		PageSize int      `json:"page_size"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Title != "" { | ||||
| 		session = session.Where("title LIKE ?", "%"+data.Title+"%") | ||||
| 	} | ||||
| 	if data.UserId > 0 { | ||||
| 		session = session.Where("user_id = ?", data.UserId) | ||||
| 	} | ||||
| 	if data.Model != "" { | ||||
| 		session = session.Where("model = ?", data.Model) | ||||
| 	} | ||||
| 	if len(data.CreateAt) == 2 { | ||||
| 		start := utils.Str2stamp(data.CreateAt[0] + " 00:00:00") | ||||
| 		end := utils.Str2stamp(data.CreateAt[1] + " 00:00:00") | ||||
| 		session = session.Where("created_at >= ? AND created_at <= ?", start, end) | ||||
| 	} | ||||
|  | ||||
| 	var total int64 | ||||
| 	session.Model(&model.ChatItem{}).Count(&total) | ||||
| 	var items []model.ChatItem | ||||
| 	var list = make([]chatItemVo, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		userIds := make([]uint, 0) | ||||
| 		chatIds := make([]string, 0) | ||||
| 		roleIds := make([]uint, 0) | ||||
| 		for _, item := range items { | ||||
| 			userIds = append(userIds, item.UserId) | ||||
| 			chatIds = append(chatIds, item.ChatId) | ||||
| 			roleIds = append(roleIds, item.RoleId) | ||||
| 		} | ||||
| 		var messages []model.ChatMessage | ||||
| 		var users []model.User | ||||
| 		var roles []model.ChatRole | ||||
| 		h.DB.Where("chat_id IN ?", chatIds).Find(&messages) | ||||
| 		h.DB.Where("id IN ?", userIds).Find(&users) | ||||
| 		h.DB.Where("id IN ?", roleIds).Find(&roles) | ||||
|  | ||||
| 		tokenMap := make(map[string]int) | ||||
| 		userMap := make(map[uint]string) | ||||
| 		msgMap := make(map[string]int) | ||||
| 		roleMap := make(map[uint]vo.ChatRole) | ||||
| 		for _, msg := range messages { | ||||
| 			tokenMap[msg.ChatId] += msg.Tokens | ||||
| 			msgMap[msg.ChatId] += 1 | ||||
| 		} | ||||
| 		for _, user := range users { | ||||
| 			userMap[user.Id] = user.Username | ||||
| 		} | ||||
| 		for _, r := range roles { | ||||
| 			var roleVo vo.ChatRole | ||||
| 			err := utils.CopyObject(r, &roleVo) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			roleMap[r.Id] = roleVo | ||||
| 		} | ||||
| 		for _, item := range items { | ||||
| 			list = append(list, chatItemVo{ | ||||
| 				UserId:    item.UserId, | ||||
| 				Username:  userMap[item.UserId], | ||||
| 				ChatId:    item.ChatId, | ||||
| 				Title:     item.Title, | ||||
| 				Model:     item.Model, | ||||
| 				Token:     tokenMap[item.ChatId], | ||||
| 				MsgNum:    msgMap[item.ChatId], | ||||
| 				Role:      roleMap[item.RoleId], | ||||
| 				CreatedAt: item.CreatedAt.Unix(), | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list)) | ||||
| } | ||||
|  | ||||
| type chatMessageVo struct { | ||||
| 	Id        uint   `json:"id"` | ||||
| 	UserId    uint   `json:"user_id"` | ||||
| 	Username  string `json:"username"` | ||||
| 	Content   string `json:"content"` | ||||
| 	Type      string `json:"type"` | ||||
| 	Model     string `json:"model"` | ||||
| 	Token     int    `json:"token"` | ||||
| 	Icon      string `json:"icon"` | ||||
| 	CreatedAt int64  `json:"created_at"` | ||||
| } | ||||
|  | ||||
| // Messages 读取聊天记录列表 | ||||
| func (h *ChatHandler) Messages(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		UserId   uint     `json:"user_id"` | ||||
| 		Content  string   `json:"content"` | ||||
| 		Model    string   `json:"model"` | ||||
| 		CreateAt []string `json:"created_time"` | ||||
| 		Page     int      `json:"page"` | ||||
| 		PageSize int      `json:"page_size"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Content != "" { | ||||
| 		session = session.Where("content LIKE ?", "%"+data.Content+"%") | ||||
| 	} | ||||
| 	if data.UserId > 0 { | ||||
| 		session = session.Where("user_id = ?", data.UserId) | ||||
| 	} | ||||
| 	if data.Model != "" { | ||||
| 		session = session.Where("model = ?", data.Model) | ||||
| 	} | ||||
| 	if len(data.CreateAt) == 2 { | ||||
| 		start := utils.Str2stamp(data.CreateAt[0] + " 00:00:00") | ||||
| 		end := utils.Str2stamp(data.CreateAt[1] + " 00:00:00") | ||||
| 		session = session.Where("created_at >= ? AND created_at <= ?", start, end) | ||||
| 	} | ||||
|  | ||||
| 	var total int64 | ||||
| 	session.Model(&model.ChatMessage{}).Count(&total) | ||||
| 	var items []model.ChatMessage | ||||
| 	var list = make([]chatMessageVo, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		userIds := make([]uint, 0) | ||||
| 		for _, item := range items { | ||||
| 			userIds = append(userIds, item.UserId) | ||||
| 		} | ||||
| 		var users []model.User | ||||
| 		h.DB.Where("id IN ?", userIds).Find(&users) | ||||
| 		userMap := make(map[uint]string) | ||||
| 		for _, user := range users { | ||||
| 			userMap[user.Id] = user.Username | ||||
| 		} | ||||
| 		for _, item := range items { | ||||
| 			list = append(list, chatMessageVo{ | ||||
| 				Id:        item.Id, | ||||
| 				UserId:    item.UserId, | ||||
| 				Username:  userMap[item.UserId], | ||||
| 				Content:   item.Content, | ||||
| 				Model:     item.Model, | ||||
| 				Token:     item.Tokens, | ||||
| 				Icon:      item.Icon, | ||||
| 				Type:      item.Type, | ||||
| 				CreatedAt: item.CreatedAt.Unix(), | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list)) | ||||
| } | ||||
|  | ||||
| // History 获取聊天历史记录 | ||||
| func (h *ChatHandler) History(c *gin.Context) { | ||||
| 	chatId := c.Query("chat_id") // 会话 ID | ||||
| 	var items []model.ChatMessage | ||||
| 	var messages = make([]vo.HistoryMessage, 0) | ||||
| 	res := h.DB.Where("chat_id = ?", chatId).Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No history message") | ||||
| 		return | ||||
| 	} else { | ||||
| 		for _, item := range items { | ||||
| 			var v vo.HistoryMessage | ||||
| 			err := utils.CopyObject(item, &v) | ||||
| 			v.CreatedAt = item.CreatedAt.Unix() | ||||
| 			v.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 			if err == nil { | ||||
| 				messages = append(messages, v) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, messages) | ||||
| } | ||||
|  | ||||
| // RemoveChat 删除对话 | ||||
| func (h *ChatHandler) RemoveChat(c *gin.Context) { | ||||
| 	chatId := h.GetTrim(c, "chat_id") | ||||
| 	if chatId == "" { | ||||
| 		resp.ERROR(c, "请传入 ChatId") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tx := h.DB.Begin() | ||||
| 	// 删除聊天记录 | ||||
| 	res := tx.Unscoped().Debug().Where("chat_id = ?", chatId).Delete(&model.ChatMessage{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "failed to remove chat message") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 删除对话 | ||||
| 	res = tx.Unscoped().Where("chat_id = ?", chatId).Delete(model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		tx.Rollback() // 回滚 | ||||
| 		resp.ERROR(c, "failed to remove chat") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tx.Commit() | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // RemoveMessage 删除聊天记录 | ||||
| func (h *ChatHandler) RemoveMessage(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	err := h.DB.Unscoped().Where("id = ?", id).Delete(&model.ChatMessage{}).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										193
									
								
								api/handler/admin/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								api/handler/admin/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,193 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatModelHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler { | ||||
| 	return &ChatModelHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| func (h *ChatModelHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id          uint    `json:"id"` | ||||
| 		Name        string  `json:"name"` | ||||
| 		Value       string  `json:"value"` | ||||
| 		Enabled     bool    `json:"enabled"` | ||||
| 		SortNum     int     `json:"sort_num"` | ||||
| 		Open        bool    `json:"open"` | ||||
| 		Platform    string  `json:"platform"` | ||||
| 		Power       int     `json:"power"` | ||||
| 		MaxTokens   int     `json:"max_tokens"`  // 最大响应长度 | ||||
| 		MaxContext  int     `json:"max_context"` // 最大上下文长度 | ||||
| 		Temperature float32 `json:"temperature"` // 模型温度 | ||||
| 		KeyId       int     `json:"key_id,omitempty"` | ||||
| 		CreatedAt   int64   `json:"created_at"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	item := model.ChatModel{} | ||||
| 	// 更新 | ||||
| 	if data.Id > 0 { | ||||
| 		h.DB.Where("id", data.Id).First(&item) | ||||
| 	} | ||||
|  | ||||
| 	item.Name = data.Name | ||||
| 	item.Value = data.Value | ||||
| 	item.Enabled = data.Enabled | ||||
| 	item.SortNum = data.SortNum | ||||
| 	item.Open = data.Open | ||||
| 	item.Power = data.Power | ||||
| 	item.MaxTokens = data.MaxTokens | ||||
| 	item.MaxContext = data.MaxContext | ||||
| 	item.Temperature = data.Temperature | ||||
| 	item.KeyId = data.KeyId | ||||
|  | ||||
| 	var res *gorm.DB | ||||
| 	if data.Id > 0 { | ||||
| 		res = h.DB.Save(&item) | ||||
| 	} else { | ||||
| 		res = h.DB.Create(&item) | ||||
| 	} | ||||
| 	if res.Error != nil { | ||||
| 		logger.Error("error with update database:", res.Error) | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var itemVo vo.ChatModel | ||||
| 	err := utils.CopyObject(item, &itemVo) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "数据拷贝失败!") | ||||
| 		return | ||||
| 	} | ||||
| 	itemVo.Id = item.Id | ||||
| 	itemVo.CreatedAt = item.CreatedAt.Unix() | ||||
| 	resp.SUCCESS(c, itemVo) | ||||
| } | ||||
|  | ||||
| // List 模型列表 | ||||
| func (h *ChatModelHandler) List(c *gin.Context) { | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	enable := h.GetBool(c, "enable") | ||||
| 	name := h.GetTrim(c, "name") | ||||
| 	if enable { | ||||
| 		session = session.Where("enabled", enable) | ||||
| 	} | ||||
| 	if name != "" { | ||||
| 		session = session.Where("name LIKE ?", name+"%") | ||||
| 	} | ||||
| 	var items []model.ChatModel | ||||
| 	var cms = make([]vo.ChatModel, 0) | ||||
| 	res := session.Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.SUCCESS(c, cms) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// initialize key name | ||||
| 	keyIds := make([]int, 0) | ||||
| 	for _, v := range items { | ||||
| 		keyIds = append(keyIds, v.KeyId) | ||||
| 	} | ||||
| 	var keys []model.ApiKey | ||||
| 	keyMap := make(map[uint]string) | ||||
| 	h.DB.Where("id IN ?", keyIds).Find(&keys) | ||||
| 	for _, v := range keys { | ||||
| 		keyMap[v.Id] = v.Name | ||||
| 	} | ||||
| 	for _, item := range items { | ||||
| 		var cm vo.ChatModel | ||||
| 		err := utils.CopyObject(item, &cm) | ||||
| 		if err == nil { | ||||
| 			cm.Id = item.Id | ||||
| 			cm.CreatedAt = item.CreatedAt.Unix() | ||||
| 			cm.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 			cm.KeyName = keyMap[uint(item.KeyId)] | ||||
| 			cms = append(cms, cm) | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, cms) | ||||
| } | ||||
|  | ||||
| func (h *ChatModelHandler) Set(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id    uint        `json:"id"` | ||||
| 		Filed string      `json:"filed"` | ||||
| 		Value interface{} `json:"value"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ChatModelHandler) Sort(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Ids   []uint `json:"ids"` | ||||
| 		Sorts []int  `json:"sorts"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for index, id := range data.Ids { | ||||
| 		err := h.DB.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ChatModelHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	if id <= 0 { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Where("id = ?", id).Delete(&model.ChatModel{}).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										209
									
								
								api/handler/admin/config_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								api/handler/admin/config_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/service" | ||||
| 	"geekai/store" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/shirou/gopsutil/host" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ConfigHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	levelDB        *store.LevelDB | ||||
| 	licenseService *service.LicenseService | ||||
| } | ||||
|  | ||||
| func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService) *ConfigHandler { | ||||
| 	return &ConfigHandler{ | ||||
| 		BaseHandler:    handler.BaseHandler{App: app, DB: db}, | ||||
| 		levelDB:        levelDB, | ||||
| 		licenseService: licenseService, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *ConfigHandler) Update(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Key    string `json:"key"` | ||||
| 		Config struct { | ||||
| 			types.SystemConfig | ||||
| 			Content string `json:"content,omitempty"` | ||||
| 			Updated bool   `json:"updated,omitempty"` | ||||
| 		} `json:"config"` | ||||
| 		ConfigBak types.SystemConfig `json:"config_bak,omitempty"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// ONLY authorized user can change the copyright | ||||
| 	if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy { | ||||
| 		resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	value := utils.JsonEncode(&data.Config) | ||||
| 	config := model.Config{Key: data.Key, Config: value} | ||||
| 	res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if config.Id > 0 { | ||||
| 		config.Config = value | ||||
| 		res := h.DB.Updates(&config) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// update config cache for AppServer | ||||
| 		var cfg model.Config | ||||
| 		h.DB.Where("marker", data.Key).First(&cfg) | ||||
| 		var err error | ||||
| 		if data.Key == "system" { | ||||
| 			err = utils.JsonDecode(cfg.Config, &h.App.SysConfig) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, "Failed to update config cache: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		logger.Infof("Update AppServer's config successfully: %v", config.Config) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, config) | ||||
| } | ||||
|  | ||||
| // Get 获取指定的系统配置 | ||||
| func (h *ConfigHandler) Get(c *gin.Context) { | ||||
| 	key := c.Query("key") | ||||
| 	var config model.Config | ||||
| 	res := h.DB.Where("marker", key).First(&config) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var value map[string]interface{} | ||||
| 	err := utils.JsonDecode(config.Config, &value) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	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 | ||||
| 	} | ||||
| 	info, err := host.Info() | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = h.licenseService.ActiveLicense(data.License, info.HostID) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, info.HostID) | ||||
| } | ||||
|  | ||||
| // GetLicense 获取 License 信息 | ||||
| func (h *ConfigHandler) GetLicense(c *gin.Context) { | ||||
| 	license := h.licenseService.GetLicense() | ||||
| 	resp.SUCCESS(c, license) | ||||
| } | ||||
|  | ||||
| // FixData 修复数据 | ||||
| func (h *ConfigHandler) FixData(c *gin.Context) { | ||||
| 	resp.ERROR(c, "当前升级版本没有数据需要修正!") | ||||
| 	return | ||||
| 	//var fixed bool | ||||
| 	//version := "data_fix_4.1.4" | ||||
| 	//err := h.levelDB.Get(version, &fixed) | ||||
| 	//if err == nil || fixed { | ||||
| 	//	resp.ERROR(c, "当前版本数据修复已完成,请不要重复执行操作") | ||||
| 	//	return | ||||
| 	//} | ||||
| 	//tx := h.DB.Begin() | ||||
| 	//var users []model.User | ||||
| 	//err = tx.Find(&users).Error | ||||
| 	//if err != nil { | ||||
| 	//	resp.ERROR(c, err.Error()) | ||||
| 	//	return | ||||
| 	//} | ||||
| 	//for _, user := range users { | ||||
| 	//	if user.Email != "" || user.Mobile != "" { | ||||
| 	//		continue | ||||
| 	//	} | ||||
| 	//	if utils.IsValidEmail(user.Username) { | ||||
| 	//		user.Email = user.Username | ||||
| 	//	} else if utils.IsValidMobile(user.Username) { | ||||
| 	//		user.Mobile = user.Username | ||||
| 	//	} | ||||
| 	//	err = tx.Save(&user).Error | ||||
| 	//	if err != nil { | ||||
| 	//		resp.ERROR(c, err.Error()) | ||||
| 	//		tx.Rollback() | ||||
| 	//		return | ||||
| 	//	} | ||||
| 	//} | ||||
| 	// | ||||
| 	//var orders []model.Order | ||||
| 	//err = h.DB.Find(&orders).Error | ||||
| 	//if err != nil { | ||||
| 	//	resp.ERROR(c, err.Error()) | ||||
| 	//	return | ||||
| 	//} | ||||
| 	//for _, order := range orders { | ||||
| 	//	if order.PayWay == "支付宝" { | ||||
| 	//		order.PayWay = "alipay" | ||||
| 	//		order.PayType = "alipay" | ||||
| 	//	} else if order.PayWay == "微信支付" { | ||||
| 	//		order.PayWay = "wechat" | ||||
| 	//		order.PayType = "wxpay" | ||||
| 	//	} else if order.PayWay == "hupi" { | ||||
| 	//		order.PayType = "wxpay" | ||||
| 	//	} | ||||
| 	//	err = tx.Save(&order).Error | ||||
| 	//	if err != nil { | ||||
| 	//		resp.ERROR(c, err.Error()) | ||||
| 	//		tx.Rollback() | ||||
| 	//		return | ||||
| 	//	} | ||||
| 	//} | ||||
| 	//tx.Commit() | ||||
| 	//err = h.levelDB.Put(version, true) | ||||
| 	//if err != nil { | ||||
| 	//	resp.ERROR(c, err.Error()) | ||||
| 	//	return | ||||
| 	//} | ||||
| 	//resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										110
									
								
								api/handler/admin/dashboard_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								api/handler/admin/dashboard_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/shopspring/decimal" | ||||
| 	"gorm.io/gorm" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type DashboardHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler { | ||||
| 	return &DashboardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| type statsVo struct { | ||||
| 	Users  int64                         `json:"users"` | ||||
| 	Chats  int64                         `json:"chats"` | ||||
| 	Tokens int                           `json:"tokens"` | ||||
| 	Income float64                       `json:"income"` | ||||
| 	Chart  map[string]map[string]float64 `json:"chart"` | ||||
| } | ||||
|  | ||||
| func (h *DashboardHandler) Stats(c *gin.Context) { | ||||
| 	stats := statsVo{} | ||||
| 	// new users statistic | ||||
| 	var userCount int64 | ||||
| 	now := time.Now() | ||||
| 	zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) | ||||
| 	res := h.DB.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount) | ||||
| 	if res.Error == nil { | ||||
| 		stats.Users = userCount | ||||
| 	} | ||||
|  | ||||
| 	// new chats statistic | ||||
| 	var chatCount int64 | ||||
| 	res = h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount) | ||||
| 	if res.Error == nil { | ||||
| 		stats.Chats = chatCount | ||||
| 	} | ||||
|  | ||||
| 	// tokens took stats | ||||
| 	var historyMessages []model.ChatMessage | ||||
| 	res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages) | ||||
| 	for _, item := range historyMessages { | ||||
| 		stats.Tokens += item.Tokens | ||||
| 	} | ||||
|  | ||||
| 	// 订单收入 | ||||
| 	var orders []model.Order | ||||
| 	res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders) | ||||
| 	for _, item := range orders { | ||||
| 		stats.Income += item.Amount | ||||
| 	} | ||||
|  | ||||
| 	// 统计7天的订单的图表 | ||||
| 	startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02") | ||||
| 	var statsChart = make(map[string]map[string]float64) | ||||
| 	//// 初始化 | ||||
| 	var userStatistic, historyMessagesStatistic, incomeStatistic = make(map[string]float64), make(map[string]float64), make(map[string]float64) | ||||
| 	for i := 0; i < 7; i++ { | ||||
| 		var initTime = time.Date(now.Year(), now.Month(), now.Day()-i, 0, 0, 0, 0, now.Location()).Format("2006-01-02") | ||||
| 		userStatistic[initTime] = float64(0) | ||||
| 		historyMessagesStatistic[initTime] = float64(0) | ||||
| 		incomeStatistic[initTime] = float64(0) | ||||
| 	} | ||||
|  | ||||
| 	// 统计用户7天增加的曲线 | ||||
| 	var users []model.User | ||||
| 	res = h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range users { | ||||
| 			userStatistic[item.CreatedAt.Format("2006-01-02")] += 1 | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 统计7天Token 消耗 | ||||
| 	res = h.DB.Where("created_at > ?", startDate).Find(&historyMessages) | ||||
| 	for _, item := range historyMessages { | ||||
| 		historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens) | ||||
| 	} | ||||
|  | ||||
| 	// 统计最近7天的订单 | ||||
| 	res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders) | ||||
| 	for _, item := range orders { | ||||
| 		incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64() | ||||
| 	} | ||||
|  | ||||
| 	statsChart["users"] = userStatistic | ||||
| 	statsChart["historyMessage"] = historyMessagesStatistic | ||||
| 	statsChart["orders"] = incomeStatistic | ||||
|  | ||||
| 	stats.Chart = statsChart | ||||
|  | ||||
| 	resp.SUCCESS(c, stats) | ||||
| } | ||||
							
								
								
									
										128
									
								
								api/handler/admin/function_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								api/handler/admin/function_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type FunctionHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewFunctionHandler(app *core.AppServer, db *gorm.DB) *FunctionHandler { | ||||
| 	return &FunctionHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| func (h *FunctionHandler) Save(c *gin.Context) { | ||||
| 	var data vo.Function | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var f = model.Function{ | ||||
| 		Id:          data.Id, | ||||
| 		Name:        data.Name, | ||||
| 		Label:       data.Label, | ||||
| 		Description: data.Description, | ||||
| 		Parameters:  utils.JsonEncode(data.Parameters), | ||||
| 		Action:      data.Action, | ||||
| 		Token:       data.Token, | ||||
| 		Enabled:     data.Enabled, | ||||
| 	} | ||||
|  | ||||
| 	res := h.DB.Save(&f) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "error with save data:"+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	data.Id = f.Id | ||||
| 	resp.SUCCESS(c, data) | ||||
| } | ||||
|  | ||||
| func (h *FunctionHandler) Set(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id    uint        `json:"id"` | ||||
| 		Filed string      `json:"filed"` | ||||
| 		Value interface{} `json:"value"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Model(&model.Function{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *FunctionHandler) List(c *gin.Context) { | ||||
| 	var items []model.Function | ||||
| 	res := h.DB.Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No data found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	functions := make([]vo.Function, 0) | ||||
| 	for _, v := range items { | ||||
| 		var f vo.Function | ||||
| 		err := utils.CopyObject(v, &f) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		functions = append(functions, f) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, functions) | ||||
| } | ||||
|  | ||||
| func (h *FunctionHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
|  | ||||
| 	if id > 0 { | ||||
| 		err := h.DB.Delete(&model.Function{Id: uint(id)}).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // GenToken generate function api access token | ||||
| func (h *FunctionHandler) GenToken(c *gin.Context) { | ||||
| 	// 创建 token | ||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | ||||
| 		"user_id": 0, | ||||
| 		"expired": 0, | ||||
| 	}) | ||||
| 	tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) | ||||
| 	if err != nil { | ||||
| 		logger.Error("error with generate token", err) | ||||
| 		resp.ERROR(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, tokenString) | ||||
| } | ||||
							
								
								
									
										254
									
								
								api/handler/admin/image_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								api/handler/admin/image_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,254 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/service" | ||||
| 	"geekai/service/oss" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ImageHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	userService *service.UserService | ||||
| 	uploader    *oss.UploaderManager | ||||
| } | ||||
|  | ||||
| func NewImageHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *ImageHandler { | ||||
| 	return &ImageHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager} | ||||
| } | ||||
|  | ||||
| type imageQuery struct { | ||||
| 	Prompt    string   `json:"prompt"` | ||||
| 	Username  string   `json:"username"` | ||||
| 	CreatedAt []string `json:"created_at"` | ||||
| 	Page      int      `json:"page"` | ||||
| 	PageSize  int      `json:"page_size"` | ||||
| } | ||||
|  | ||||
| // MjList Midjourney 任务列表 | ||||
| func (h *ImageHandler) MjList(c *gin.Context) { | ||||
| 	var data imageQuery | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Username != "" { | ||||
| 		var user model.User | ||||
| 		err := h.DB.Where("username", data.Username).First(&user).Error | ||||
| 		if err == nil { | ||||
| 			session = session.Where("user_id", user.Id) | ||||
| 		} | ||||
| 	} | ||||
| 	if data.Prompt != "" { | ||||
| 		session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") | ||||
| 	} | ||||
| 	if len(data.CreatedAt) == 2 { | ||||
| 		session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) | ||||
| 	} | ||||
| 	var total int64 | ||||
| 	session.Model(&model.MidJourneyJob{}).Count(&total) | ||||
| 	var list []model.MidJourneyJob | ||||
| 	var items = make([]vo.MidJourneyJob, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error | ||||
| 	if err == nil { | ||||
| 		// 填充数据 | ||||
| 		for _, item := range list { | ||||
| 			var job vo.MidJourneyJob | ||||
| 			err = utils.CopyObject(item, &job) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			job.CreatedAt = item.CreatedAt.Unix() | ||||
| 			items = append(items, job) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) | ||||
| } | ||||
|  | ||||
| // SdList Stable Diffusion 任务列表 | ||||
| func (h *ImageHandler) SdList(c *gin.Context) { | ||||
| 	var data imageQuery | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Username != "" { | ||||
| 		var user model.User | ||||
| 		err := h.DB.Where("username", data.Username).First(&user).Error | ||||
| 		if err == nil { | ||||
| 			session = session.Where("user_id", user.Id) | ||||
| 		} | ||||
| 	} | ||||
| 	if data.Prompt != "" { | ||||
| 		session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") | ||||
| 	} | ||||
| 	if len(data.CreatedAt) == 2 { | ||||
| 		session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) | ||||
| 	} | ||||
| 	var total int64 | ||||
| 	session.Model(&model.SdJob{}).Count(&total) | ||||
| 	var list []model.SdJob | ||||
| 	var items = make([]vo.SdJob, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error | ||||
| 	if err == nil { | ||||
| 		// 填充数据 | ||||
| 		for _, item := range list { | ||||
| 			var job vo.SdJob | ||||
| 			err = utils.CopyObject(item, &job) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			job.CreatedAt = item.CreatedAt.Unix() | ||||
| 			items = append(items, job) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) | ||||
| } | ||||
|  | ||||
| // DallList DALL-E 任务列表 | ||||
| func (h *ImageHandler) DallList(c *gin.Context) { | ||||
| 	var data imageQuery | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Username != "" { | ||||
| 		var user model.User | ||||
| 		err := h.DB.Where("username", data.Username).First(&user).Error | ||||
| 		if err == nil { | ||||
| 			session = session.Where("user_id", user.Id) | ||||
| 		} | ||||
| 	} | ||||
| 	if data.Prompt != "" { | ||||
| 		session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") | ||||
| 	} | ||||
| 	if len(data.CreatedAt) == 2 { | ||||
| 		session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) | ||||
| 	} | ||||
| 	var total int64 | ||||
| 	session.Model(&model.DallJob{}).Count(&total) | ||||
| 	var list []model.DallJob | ||||
| 	var items = make([]vo.DallJob, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error | ||||
| 	if err == nil { | ||||
| 		// 填充数据 | ||||
| 		for _, item := range list { | ||||
| 			var job vo.DallJob | ||||
| 			err = utils.CopyObject(item, &job) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			job.CreatedAt = item.CreatedAt.Unix() | ||||
| 			items = append(items, job) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) | ||||
| } | ||||
|  | ||||
| func (h *ImageHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	tab := c.Query("tab") | ||||
|  | ||||
| 	tx := h.DB.Begin() | ||||
| 	var md, remark, imgURL string | ||||
| 	var power, userId, progress int | ||||
| 	switch tab { | ||||
| 	case "mj": | ||||
| 		var job model.MidJourneyJob | ||||
| 		if err := h.DB.Where("id", id).First(&job).Error; err != nil { | ||||
| 			resp.ERROR(c, "记录不存在") | ||||
| 			return | ||||
| 		} | ||||
| 		tx.Delete(&job) | ||||
| 		md = "mid-journey" | ||||
| 		power = job.Power | ||||
| 		userId = job.UserId | ||||
| 		remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) | ||||
| 		progress = job.Progress | ||||
| 		imgURL = job.ImgURL | ||||
| 		break | ||||
| 	case "sd": | ||||
| 		var job model.SdJob | ||||
| 		if res := h.DB.Where("id", id).First(&job); res.Error != nil { | ||||
| 			resp.ERROR(c, "记录不存在") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// 删除任务 | ||||
| 		tx.Delete(&job) | ||||
| 		md = "stable-diffusion" | ||||
| 		power = job.Power | ||||
| 		userId = job.UserId | ||||
| 		remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) | ||||
| 		progress = job.Progress | ||||
| 		imgURL = job.ImgURL | ||||
| 		break | ||||
| 	case "dall": | ||||
| 		var job model.DallJob | ||||
| 		if res := h.DB.Where("id", id).First(&job); res.Error != nil { | ||||
| 			resp.ERROR(c, "记录不存在") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// 删除任务 | ||||
| 		tx.Delete(&job) | ||||
| 		md = "dall-e-3" | ||||
| 		power = job.Power | ||||
| 		userId = int(job.UserId) | ||||
| 		remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) | ||||
| 		progress = job.Progress | ||||
| 		imgURL = job.ImgURL | ||||
| 		break | ||||
| 	default: | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if progress != 100 { | ||||
| 		err := h.userService.IncreasePower(userId, power, model.PowerLog{ | ||||
| 			Type:   types.PowerRefund, | ||||
| 			Model:  md, | ||||
| 			Remark: remark, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			tx.Rollback() | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	tx.Commit() | ||||
| 	// remove image | ||||
| 	err := h.uploader.GetUploadHandler().Delete(imgURL) | ||||
| 	if err != nil { | ||||
| 		logger.Error("remove image failed: ", err) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										200
									
								
								api/handler/admin/media_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								api/handler/admin/media_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/service" | ||||
| 	"geekai/service/oss" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type MediaHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	userService *service.UserService | ||||
| 	uploader    *oss.UploaderManager | ||||
| } | ||||
|  | ||||
| func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *MediaHandler { | ||||
| 	return &MediaHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager} | ||||
| } | ||||
|  | ||||
| type mediaQuery struct { | ||||
| 	Prompt    string   `json:"prompt"` | ||||
| 	Username  string   `json:"username"` | ||||
| 	CreatedAt []string `json:"created_at"` | ||||
| 	Page      int      `json:"page"` | ||||
| 	PageSize  int      `json:"page_size"` | ||||
| } | ||||
|  | ||||
| // SunoList Suno 任务列表 | ||||
| func (h *MediaHandler) SunoList(c *gin.Context) { | ||||
| 	var data mediaQuery | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Username != "" { | ||||
| 		var user model.User | ||||
| 		err := h.DB.Where("username", data.Username).First(&user).Error | ||||
| 		if err == nil { | ||||
| 			session = session.Where("user_id", user.Id) | ||||
| 		} | ||||
| 	} | ||||
| 	if data.Prompt != "" { | ||||
| 		session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") | ||||
| 	} | ||||
| 	if len(data.CreatedAt) == 2 { | ||||
| 		session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) | ||||
| 	} | ||||
| 	var total int64 | ||||
| 	session.Model(&model.SunoJob{}).Count(&total) | ||||
| 	var list []model.SunoJob | ||||
| 	var items = make([]vo.SunoJob, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error | ||||
| 	if err == nil { | ||||
| 		// 填充数据 | ||||
| 		for _, item := range list { | ||||
| 			var job vo.SunoJob | ||||
| 			err = utils.CopyObject(item, &job) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			job.CreatedAt = item.CreatedAt.Unix() | ||||
| 			items = append(items, job) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) | ||||
| } | ||||
|  | ||||
| // LumaList Luma 视频任务列表 | ||||
| func (h *MediaHandler) LumaList(c *gin.Context) { | ||||
| 	var data mediaQuery | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Username != "" { | ||||
| 		var user model.User | ||||
| 		err := h.DB.Where("username", data.Username).First(&user).Error | ||||
| 		if err == nil { | ||||
| 			session = session.Where("user_id", user.Id) | ||||
| 		} | ||||
| 	} | ||||
| 	if data.Prompt != "" { | ||||
| 		session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%") | ||||
| 	} | ||||
| 	if len(data.CreatedAt) == 2 { | ||||
| 		session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) | ||||
| 	} | ||||
| 	var total int64 | ||||
| 	session.Model(&model.VideoJob{}).Count(&total) | ||||
| 	var list []model.VideoJob | ||||
| 	var items = make([]vo.VideoJob, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error | ||||
| 	if err == nil { | ||||
| 		// 填充数据 | ||||
| 		for _, item := range list { | ||||
| 			var job vo.VideoJob | ||||
| 			err = utils.CopyObject(item, &job) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			job.CreatedAt = item.CreatedAt.Unix() | ||||
| 			if job.VideoURL == "" { | ||||
| 				job.VideoURL = job.WaterURL | ||||
| 			} | ||||
| 			items = append(items, job) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) | ||||
| } | ||||
|  | ||||
| func (h *MediaHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	tab := c.Query("tab") | ||||
|  | ||||
| 	tx := h.DB.Begin() | ||||
| 	var md, remark, fileURL string | ||||
| 	var power, userId, progress int | ||||
| 	switch tab { | ||||
| 	case "suno": | ||||
| 		var job model.SunoJob | ||||
| 		if err := h.DB.Where("id", id).First(&job).Error; err != nil { | ||||
| 			resp.ERROR(c, "记录不存在") | ||||
| 			return | ||||
| 		} | ||||
| 		tx.Delete(&job) | ||||
| 		md = "suno" | ||||
| 		power = job.Power | ||||
| 		userId = job.UserId | ||||
| 		remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) | ||||
| 		progress = job.Progress | ||||
| 		fileURL = job.AudioURL | ||||
| 		break | ||||
| 	case "luma": | ||||
| 		var job model.VideoJob | ||||
| 		if res := h.DB.Where("id", id).First(&job); res.Error != nil { | ||||
| 			resp.ERROR(c, "记录不存在") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// 删除任务 | ||||
| 		tx.Delete(&job) | ||||
| 		md = job.Type | ||||
| 		power = job.Power | ||||
| 		userId = job.UserId | ||||
| 		remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg) | ||||
| 		progress = job.Progress | ||||
| 		fileURL = job.VideoURL | ||||
| 		if fileURL == "" { | ||||
| 			fileURL = job.WaterURL | ||||
| 		} | ||||
| 		break | ||||
| 	default: | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if progress != 100 { | ||||
| 		err := h.userService.IncreasePower(userId, power, model.PowerLog{ | ||||
| 			Type:   types.PowerRefund, | ||||
| 			Model:  md, | ||||
| 			Remark: remark, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			tx.Rollback() | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	tx.Commit() | ||||
| 	// remove image | ||||
| 	err := h.uploader.GetUploadHandler().Delete(fileURL) | ||||
| 	if err != nil { | ||||
| 		logger.Error("remove image failed: ", err) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										128
									
								
								api/handler/admin/menu_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								api/handler/admin/menu_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type MenuHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler { | ||||
| 	return &MenuHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| func (h *MenuHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id      uint   `json:"id"` | ||||
| 		Name    string `json:"name"` | ||||
| 		Icon    string `json:"icon"` | ||||
| 		URL     string `json:"url"` | ||||
| 		SortNum int    `json:"sort_num"` | ||||
| 		Enabled bool   `json:"enabled"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Save(&model.Menu{ | ||||
| 		Id:      data.Id, | ||||
| 		Name:    data.Name, | ||||
| 		Icon:    data.Icon, | ||||
| 		URL:     data.URL, | ||||
| 		SortNum: data.SortNum, | ||||
| 		Enabled: data.Enabled, | ||||
| 	}).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // List 数据列表 | ||||
| func (h *MenuHandler) List(c *gin.Context) { | ||||
| 	var items []model.Menu | ||||
| 	var list = make([]vo.Menu, 0) | ||||
| 	res := h.DB.Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var product vo.Menu | ||||
| 			err := utils.CopyObject(item, &product) | ||||
| 			if err == nil { | ||||
| 				list = append(list, product) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, list) | ||||
| } | ||||
|  | ||||
| func (h *MenuHandler) Enable(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id      uint `json:"id"` | ||||
| 		Enabled bool `json:"enabled"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *MenuHandler) Sort(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Ids   []uint `json:"ids"` | ||||
| 		Sorts []int  `json:"sorts"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for index, id := range data.Ids { | ||||
| 		err := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *MenuHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
|  | ||||
| 	if id > 0 { | ||||
| 		err := h.DB.Where("id", id).Delete(&model.Menu{}).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										135
									
								
								api/handler/admin/order_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								api/handler/admin/order_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type OrderHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler { | ||||
| 	return &OrderHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| func (h *OrderHandler) List(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		OrderNo  string   `json:"order_no"` | ||||
| 		Status   int      `json:"status"` | ||||
| 		PayTime  []string `json:"pay_time"` | ||||
| 		Page     int      `json:"page"` | ||||
| 		PageSize int      `json:"page_size"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.OrderNo != "" { | ||||
| 		session = session.Where("order_no", data.OrderNo) | ||||
| 	} | ||||
| 	if len(data.PayTime) == 2 { | ||||
| 		start := utils.Str2stamp(data.PayTime[0] + " 00:00:00") | ||||
| 		end := utils.Str2stamp(data.PayTime[1] + " 00:00:00") | ||||
| 		session = session.Where("pay_time >= ? AND pay_time <= ?", start, end) | ||||
| 	} | ||||
| 	if data.Status >= 0 { | ||||
| 		session = session.Where("status", data.Status) | ||||
| 	} | ||||
| 	var total int64 | ||||
| 	session.Model(&model.Order{}).Count(&total) | ||||
| 	var items []model.Order | ||||
| 	var list = make([]vo.Order, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var order vo.Order | ||||
| 			err := utils.CopyObject(item, &order) | ||||
| 			if err == nil { | ||||
| 				order.Id = item.Id | ||||
| 				order.CreatedAt = item.CreatedAt.Unix() | ||||
| 				order.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				payMethod, ok := types.PayMethods[item.PayWay] | ||||
| 				if !ok { | ||||
| 					payMethod = item.PayWay | ||||
| 				} | ||||
| 				payName, ok := types.PayNames[item.PayType] | ||||
| 				if !ok { | ||||
| 					payName = item.PayWay | ||||
| 				} | ||||
| 				order.PayMethod = payMethod | ||||
| 				order.PayName = payName | ||||
| 				list = append(list, order) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list)) | ||||
| } | ||||
|  | ||||
| func (h *OrderHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
|  | ||||
| 	if id > 0 { | ||||
| 		var item model.Order | ||||
| 		res := h.DB.First(&item, id) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, "记录不存在!") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if item.Status == types.OrderPaidSuccess { | ||||
| 			resp.ERROR(c, "已支付订单不允许删除!") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		err := h.DB.Where("id = ?", id).Delete(&model.Order{}).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *OrderHandler) Clear(c *gin.Context) { | ||||
| 	var orders []model.Order | ||||
| 	err := h.DB.Where("status <> ?", 2).Where("pay_time", 0).Find(&orders).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	deleteIds := make([]uint, 0) | ||||
| 	for _, order := range orders { | ||||
| 		// 只删除 15 分钟内的未支付订单 | ||||
| 		if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) { | ||||
| 			deleteIds = append(deleteIds, order.Id) | ||||
| 		} | ||||
| 	} | ||||
| 	err = h.DB.Where("id IN ?", deleteIds).Delete(&model.Order{}).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										84
									
								
								api/handler/admin/power_log_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								api/handler/admin/power_log_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type PowerLogHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler { | ||||
| 	return &PowerLogHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| func (h *PowerLogHandler) List(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Username string   `json:"username"` | ||||
| 		Type     int      `json:"type"` | ||||
| 		Model    string   `json:"model"` | ||||
| 		Date     []string `json:"date"` | ||||
| 		Page     int      `json:"page"` | ||||
| 		PageSize int      `json:"page_size"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Model != "" { | ||||
| 		session = session.Where("model", data.Model) | ||||
| 	} | ||||
| 	if data.Type > 0 { | ||||
| 		session = session.Where("type", data.Type) | ||||
| 	} | ||||
| 	if len(data.Date) == 2 { | ||||
| 		start := data.Date[0] + " 00:00:00" | ||||
| 		end := data.Date[1] + " 00:00:00" | ||||
| 		session = session.Where("created_at >= ? AND created_at <= ?", start, end) | ||||
| 	} | ||||
|  | ||||
| 	var total int64 | ||||
| 	session.Model(&model.PowerLog{}).Count(&total) | ||||
| 	var items []model.PowerLog | ||||
| 	var list = make([]vo.PowerLog, 0) | ||||
| 	offset := (data.Page - 1) * data.PageSize | ||||
| 	res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var log vo.PowerLog | ||||
| 			err := utils.CopyObject(item, &log) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			log.Id = item.Id | ||||
| 			log.CreatedAt = item.CreatedAt.Unix() | ||||
| 			log.TypeStr = item.Type.String() | ||||
| 			list = append(list, log) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 统计消费算力总和 | ||||
| 	var totalPower float64 | ||||
| 	if len(data.Date) == 2 { | ||||
| 		session.Where("mark", 0).Select("SUM(amount) as total_sum").Scan(&totalPower) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, gin.H{"data": vo.NewPage(total, data.Page, data.PageSize, list), "stat": totalPower}) | ||||
| } | ||||
							
								
								
									
										149
									
								
								api/handler/admin/product_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								api/handler/admin/product_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type ProductHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler { | ||||
| 	return &ProductHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| func (h *ProductHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id        uint    `json:"id"` | ||||
| 		Name      string  `json:"name"` | ||||
| 		Price     float64 `json:"price"` | ||||
| 		Discount  float64 `json:"discount"` | ||||
| 		Enabled   bool    `json:"enabled"` | ||||
| 		Days      int     `json:"days"` | ||||
| 		Power     int     `json:"power"` | ||||
| 		CreatedAt int64   `json:"created_at"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	item := model.Product{ | ||||
| 		Name:     data.Name, | ||||
| 		Price:    data.Price, | ||||
| 		Discount: data.Discount, | ||||
| 		Days:     data.Days, | ||||
| 		Power:    data.Power, | ||||
| 		Enabled:  data.Enabled} | ||||
| 	item.Id = data.Id | ||||
| 	if item.Id > 0 { | ||||
| 		item.CreatedAt = time.Unix(data.CreatedAt, 0) | ||||
| 	} | ||||
| 	err := h.DB.Save(&item).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var itemVo vo.Product | ||||
| 	err = utils.CopyObject(item, &itemVo) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "数据拷贝失败: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	itemVo.Id = item.Id | ||||
| 	itemVo.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 	resp.SUCCESS(c, itemVo) | ||||
| } | ||||
|  | ||||
| // List 数据列表 | ||||
| func (h *ProductHandler) List(c *gin.Context) { | ||||
| 	var items []model.Product | ||||
| 	var list = make([]vo.Product, 0) | ||||
| 	res := h.DB.Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var product vo.Product | ||||
| 			err := utils.CopyObject(item, &product) | ||||
| 			if err == nil { | ||||
| 				product.Id = item.Id | ||||
| 				product.CreatedAt = item.CreatedAt.Unix() | ||||
| 				product.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				list = append(list, product) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, list) | ||||
| } | ||||
|  | ||||
| func (h *ProductHandler) Enable(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id      uint `json:"id"` | ||||
| 		Enabled bool `json:"enabled"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Model(&model.Product{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ProductHandler) Sort(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Ids   []uint `json:"ids"` | ||||
| 		Sorts []int  `json:"sorts"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for index, id := range data.Ids { | ||||
| 		err := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ProductHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
|  | ||||
| 	if id > 0 { | ||||
| 		err := h.DB.Where("id", id).Delete(&model.Product{}).Error | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										219
									
								
								api/handler/admin/redeem_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								api/handler/admin/redeem_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"encoding/csv" | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type RedeemHandler struct { | ||||
| 	handler.BaseHandler | ||||
| } | ||||
|  | ||||
| func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler { | ||||
| 	return &RedeemHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| func (h *RedeemHandler) List(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 1) | ||||
| 	pageSize := h.GetInt(c, "page_size", 20) | ||||
| 	code := c.Query("code") | ||||
| 	status := h.GetInt(c, "status", -1) | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if code != "" { | ||||
| 		session = session.Where("code LIKE ?", "%"+code+"%") | ||||
| 	} | ||||
| 	if status >= 0 { | ||||
| 		session = session.Where("redeemed_at", status) | ||||
| 	} | ||||
|  | ||||
| 	var total int64 | ||||
| 	session.Model(&model.Redeem{}).Count(&total) | ||||
| 	var redeems []model.Redeem | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	err := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&redeems).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	var items = make([]vo.Redeem, 0) | ||||
| 	userIds := make([]uint, 0) | ||||
| 	for _, v := range redeems { | ||||
| 		userIds = append(userIds, v.UserId) | ||||
| 	} | ||||
| 	var users []model.User | ||||
| 	h.DB.Where("id IN ?", userIds).Find(&users) | ||||
| 	var userMap = make(map[uint]model.User) | ||||
| 	for _, u := range users { | ||||
| 		userMap[u.Id] = u | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range redeems { | ||||
| 		var r vo.Redeem | ||||
| 		err = utils.CopyObject(v, &r) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		r.Id = v.Id | ||||
| 		r.Username = userMap[v.UserId].Username | ||||
| 		r.CreatedAt = v.CreatedAt.Unix() | ||||
| 		items = append(items, r) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items)) | ||||
| } | ||||
|  | ||||
| // Export 导出 CVS 文件 | ||||
| func (h *RedeemHandler) Export(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Status int   `json:"status"` | ||||
| 		Ids    []int `json:"ids"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 	} | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if data.Status >= 0 { | ||||
| 		session = session.Where("redeemed_at", data.Status) | ||||
| 	} | ||||
| 	if len(data.Ids) > 0 { | ||||
| 		session = session.Where("id IN ?", data.Ids) | ||||
| 	} | ||||
|  | ||||
| 	var items []model.Redeem | ||||
| 	err := session.Order("id DESC").Find(&items).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 设置响应头,告诉浏览器这是一个附件,需要下载 | ||||
| 	c.Header("Content-Disposition", "attachment; filename=output.csv") | ||||
| 	c.Header("Content-Type", "text/csv") | ||||
|  | ||||
| 	// 创建一个 CSV writer | ||||
| 	writer := csv.NewWriter(c.Writer) | ||||
|  | ||||
| 	// 写入 CSV 文件的标题行 | ||||
| 	headers := []string{"名称", "兑换码", "算力", "创建时间"} | ||||
| 	if err := writer.Write(headers); err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 写入数据行 | ||||
| 	records := make([][]string, 0) | ||||
| 	for _, item := range items { | ||||
| 		records = append(records, []string{item.Name, item.Code, fmt.Sprintf("%d", item.Power), item.CreatedAt.Format("2006-01-02 15:04:05")}) | ||||
| 	} | ||||
| 	for _, record := range records { | ||||
| 		if err := writer.Write(record); err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 确保所有数据都已写入响应 | ||||
| 	writer.Flush() | ||||
| 	if err := writer.Error(); err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *RedeemHandler) Create(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Name  string `json:"name"` | ||||
| 		Power int    `json:"power"` | ||||
| 		Num   int    `json:"num"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	counter := 0 | ||||
| 	codes := make([]string, 0) | ||||
| 	var errMsg = "" | ||||
| 	if data.Num > 0 { | ||||
| 		for i := 0; i < data.Num; i++ { | ||||
| 			code, err := utils.GenRedeemCode(32) | ||||
| 			if err != nil { | ||||
| 				errMsg = err.Error() | ||||
| 				continue | ||||
| 			} | ||||
| 			err = h.DB.Create(&model.Redeem{ | ||||
| 				Code:    code, | ||||
| 				Name:    data.Name, | ||||
| 				Power:   data.Power, | ||||
| 				Enabled: true, | ||||
| 			}).Error | ||||
| 			if err != nil { | ||||
| 				errMsg = err.Error() | ||||
| 				continue | ||||
| 			} | ||||
| 			codes = append(codes, code) | ||||
| 			counter++ | ||||
| 		} | ||||
| 	} | ||||
| 	if counter == 0 { | ||||
| 		resp.ERROR(c, errMsg) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, gin.H{ | ||||
| 		"counter": counter, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (h *RedeemHandler) Set(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id    uint        `json:"id"` | ||||
| 		Filed string      `json:"filed"` | ||||
| 		Value interface{} `json:"value"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.DB.Model(&model.Redeem{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *RedeemHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	if id <= 0 { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	err := h.DB.Where("id", id).Delete(&model.Redeem{}).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										52
									
								
								api/handler/admin/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								api/handler/admin/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/service/oss" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type UploadHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	uploaderManager *oss.UploaderManager | ||||
| } | ||||
|  | ||||
| func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler { | ||||
| 	return &UploadHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, uploaderManager: manager} | ||||
| } | ||||
|  | ||||
| func (h *UploadHandler) Upload(c *gin.Context) { | ||||
| 	file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file") | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	userId := 0 | ||||
| 	res := h.DB.Create(&model.File{ | ||||
| 		UserId:    userId, | ||||
| 		Name:      file.Name, | ||||
| 		ObjKey:    file.ObjKey, | ||||
| 		URL:       file.URL, | ||||
| 		Ext:       file.Ext, | ||||
| 		Size:      file.Size, | ||||
| 		CreatedAt: time.Time{}, | ||||
| 	}) | ||||
| 	if res.Error != nil || res.RowsAffected == 0 { | ||||
| 		resp.ERROR(c, "error with update database: "+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, file) | ||||
| } | ||||
							
								
								
									
										313
									
								
								api/handler/admin/user_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								api/handler/admin/user_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | ||||
| package admin | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/handler" | ||||
| 	"geekai/service" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type UserHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	licenseService *service.LicenseService | ||||
| 	redis          *redis.Client | ||||
| } | ||||
|  | ||||
| func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService, redisCli *redis.Client) *UserHandler { | ||||
| 	return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService, redis: redisCli} | ||||
| } | ||||
|  | ||||
| // List 用户列表 | ||||
| func (h *UserHandler) List(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 1) | ||||
| 	pageSize := h.GetInt(c, "page_size", 20) | ||||
| 	username := h.GetTrim(c, "username") | ||||
|  | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	var items []model.User | ||||
| 	var users = make([]vo.User, 0) | ||||
| 	var total int64 | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if username != "" { | ||||
| 		session = session.Where("username LIKE ?", "%"+username+"%") | ||||
| 	} | ||||
|  | ||||
| 	session.Model(&model.User{}).Count(&total) | ||||
| 	res := session.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var user vo.User | ||||
| 			err := utils.CopyObject(item, &user) | ||||
| 			if err == nil { | ||||
| 				user.Id = item.Id | ||||
| 				user.CreatedAt = item.CreatedAt.Unix() | ||||
| 				user.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				users = append(users, user) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	pageVo := vo.NewPage(total, page, pageSize, users) | ||||
| 	resp.SUCCESS(c, pageVo) | ||||
| } | ||||
|  | ||||
| func (h *UserHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id          uint     `json:"id"` | ||||
| 		Password    string   `json:"password"` | ||||
| 		Username    string   `json:"username"` | ||||
| 		Mobile      string   `json:"mobile"` | ||||
| 		Email       string   `json:"email"` | ||||
| 		ChatRoles   []string `json:"chat_roles"` | ||||
| 		ChatModels  []int    `json:"chat_models"` | ||||
| 		ExpiredTime string   `json:"expired_time"` | ||||
| 		Status      bool     `json:"status"` | ||||
| 		Vip         bool     `json:"vip"` | ||||
| 		Power       int      `json:"power"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	// 检测最大注册人数 | ||||
| 	var totalUser int64 | ||||
| 	h.DB.Model(&model.User{}).Count(&totalUser) | ||||
| 	if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum { | ||||
| 		resp.ERROR(c, "当前注册用户数已达上限,请请升级 License") | ||||
| 		return | ||||
| 	} | ||||
| 	var user = model.User{} | ||||
| 	var res *gorm.DB | ||||
| 	var userVo vo.User | ||||
| 	if data.Id > 0 { // 更新 | ||||
| 		res = h.DB.Where("id", data.Id).First(&user) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, "user not found") | ||||
| 			return | ||||
| 		} | ||||
| 		var oldPower = user.Power | ||||
| 		user.Username = data.Username | ||||
| 		user.Email = data.Email | ||||
| 		user.Mobile = data.Mobile | ||||
| 		user.Status = data.Status | ||||
| 		user.Vip = data.Vip | ||||
| 		user.Power = data.Power | ||||
| 		user.ChatRoles = utils.JsonEncode(data.ChatRoles) | ||||
| 		user.ChatModels = utils.JsonEncode(data.ChatModels) | ||||
| 		user.ExpiredTime = utils.Str2stamp(data.ExpiredTime) | ||||
|  | ||||
| 		res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user) | ||||
|  | ||||
| 		if res.Error != nil { | ||||
| 			logger.Error("error with update database:", res.Error) | ||||
| 			resp.ERROR(c, res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		// 记录算力日志 | ||||
| 		if oldPower != user.Power { | ||||
| 			mark := types.PowerAdd | ||||
| 			amount := user.Power - oldPower | ||||
| 			if oldPower > user.Power { | ||||
| 				mark = types.PowerSub | ||||
| 				amount = oldPower - user.Power | ||||
| 			} | ||||
| 			h.DB.Create(&model.PowerLog{ | ||||
| 				UserId:    user.Id, | ||||
| 				Username:  user.Username, | ||||
| 				Type:      types.PowerGift, | ||||
| 				Amount:    amount, | ||||
| 				Balance:   user.Power, | ||||
| 				Mark:      mark, | ||||
| 				Model:     "管理员", | ||||
| 				Remark:    fmt.Sprintf("后台管理员强制修改用户算力,修改前:%d,修改后:%d, 管理员ID:%d", oldPower, user.Power, h.GetLoginUserId(c)), | ||||
| 				CreatedAt: time.Now(), | ||||
| 			}) | ||||
| 		} | ||||
| 		// 如果禁用了用户,则将用户踢下线 | ||||
| 		if user.Status == false { | ||||
| 			key := fmt.Sprintf("users/%v", user.Id) | ||||
| 			if _, err := h.redis.Del(c, key).Result(); err != nil { | ||||
| 				logger.Error("error with delete session: ", err) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		// 检查用户是否已经存在 | ||||
| 		h.DB.Where("username", data.Username).First(&user) | ||||
| 		if user.Id > 0 { | ||||
| 			resp.ERROR(c, "用户名已存在") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		salt := utils.RandString(8) | ||||
| 		u := model.User{ | ||||
| 			Username:    data.Username, | ||||
| 			Password:    utils.GenPassword(data.Password, salt), | ||||
| 			Mobile:      data.Mobile, | ||||
| 			Email:       data.Email, | ||||
| 			Avatar:      "/images/avatar/user.png", | ||||
| 			Salt:        salt, | ||||
| 			Power:       data.Power, | ||||
| 			Status:      true, | ||||
| 			ChatRoles:   utils.JsonEncode(data.ChatRoles), | ||||
| 			ChatModels:  utils.JsonEncode(data.ChatModels), | ||||
| 			ExpiredTime: utils.Str2stamp(data.ExpiredTime), | ||||
| 		} | ||||
| 		if h.licenseService.GetLicense().Configs.DeCopy { | ||||
| 			u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6)) | ||||
| 		} else { | ||||
| 			u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6)) | ||||
| 		} | ||||
| 		res = h.DB.Create(&u) | ||||
| 		_ = utils.CopyObject(u, &userVo) | ||||
| 		userVo.Id = u.Id | ||||
| 		userVo.CreatedAt = u.CreatedAt.Unix() | ||||
| 		userVo.UpdatedAt = u.UpdatedAt.Unix() | ||||
| 	} | ||||
|  | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, userVo) | ||||
| } | ||||
|  | ||||
| // ResetPass 重置密码 | ||||
| func (h *UserHandler) ResetPass(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id       uint | ||||
| 		Password string | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var user model.User | ||||
| 	res := h.DB.First(&user, data.Id) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No user found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	password := utils.GenPassword(data.Password, user.Salt) | ||||
| 	user.Password = password | ||||
| 	res = h.DB.Updates(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *UserHandler) Remove(c *gin.Context) { | ||||
| 	id := c.Query("id") | ||||
| 	ids := c.QueryArray("ids[]") | ||||
| 	if id != "" { | ||||
| 		ids = append(ids, id) | ||||
| 	} | ||||
| 	if len(ids) == 0 { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tx := h.DB.Begin() | ||||
| 	var err error | ||||
| 	for _, id = range ids { | ||||
| 		// 删除用户 | ||||
| 		if err = tx.Where("id", id).Delete(&model.User{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		// 删除聊天记录 | ||||
| 		if err = tx.Unscoped().Where("user_id = ?", id).Delete(&model.ChatItem{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		// 删除聊天历史记录 | ||||
| 		if err = tx.Unscoped().Where("user_id = ?", id).Delete(&model.ChatMessage{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		// 删除登录日志 | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.UserLoginLog{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		// 删除算力日志 | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.PowerLog{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.InviteLog{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		// 删除众筹日志 | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.Redeem{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		// 删除绘图任务 | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.SdJob{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.DallJob{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.SunoJob{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		if err = tx.Where("user_id = ?", id).Delete(&model.VideoJob{}).Error; err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		tx.Rollback() | ||||
| 		return | ||||
| 	} | ||||
| 	tx.Commit() | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *UserHandler) LoginLog(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 1) | ||||
| 	pageSize := h.GetInt(c, "page_size", 20) | ||||
| 	var total int64 | ||||
| 	h.DB.Model(&model.UserLoginLog{}).Count(&total) | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	var items []model.UserLoginLog | ||||
| 	res := h.DB.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "获取数据失败") | ||||
| 		return | ||||
| 	} | ||||
| 	var logs []vo.UserLoginLog | ||||
| 	for _, v := range items { | ||||
| 		var log vo.UserLoginLog | ||||
| 		err := utils.CopyObject(v, &log) | ||||
| 		if err == nil { | ||||
| 			log.Id = v.Id | ||||
| 			log.CreatedAt = v.CreatedAt.Unix() | ||||
| 			logs = append(logs, log) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs)) | ||||
| } | ||||
							
								
								
									
										94
									
								
								api/handler/base_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								api/handler/base_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	logger2 "geekai/logger" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/utils" | ||||
| 	"gorm.io/gorm" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| type BaseHandler struct { | ||||
| 	App *core.AppServer | ||||
| 	DB  *gorm.DB | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetTrim(c *gin.Context, key string) string { | ||||
| 	return strings.TrimSpace(c.Query(key)) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int { | ||||
| 	return utils.IntValue(c.PostForm(key), defaultValue) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int { | ||||
| 	return utils.IntValue(c.Query(key), defaultValue) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 { | ||||
| 	return utils.FloatValue(c.Query(key)) | ||||
| } | ||||
| func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 { | ||||
| 	return utils.FloatValue(c.PostForm(key)) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetBool(c *gin.Context, key string) bool { | ||||
| 	return utils.BoolValue(c.Query(key)) | ||||
| } | ||||
| func (h *BaseHandler) PostBool(c *gin.Context, key string) bool { | ||||
| 	return utils.BoolValue(c.PostForm(key)) | ||||
| } | ||||
| func (h *BaseHandler) GetUserKey(c *gin.Context) string { | ||||
| 	userId, ok := c.Get(types.LoginUserID) | ||||
| 	if !ok { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return fmt.Sprintf("users/%v", userId) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint { | ||||
| 	userId, ok := c.Get(types.LoginUserID) | ||||
| 	if !ok { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return uint(utils.IntValue(utils.InterfaceToString(userId), 0)) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) IsLogin(c *gin.Context) bool { | ||||
| 	return h.GetLoginUserId(c) > 0 | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetLoginUser(c *gin.Context) (model.User, error) { | ||||
| 	value, exists := c.Get(types.LoginUserCache) | ||||
| 	if exists { | ||||
| 		return value.(model.User), nil | ||||
| 	} | ||||
|  | ||||
| 	userId, ok := c.Get(types.LoginUserID) | ||||
| 	if !ok { | ||||
| 		return model.User{}, errors.New("user not login") | ||||
| 	} | ||||
|  | ||||
| 	var user model.User | ||||
| 	res := h.DB.Where("id", userId).First(&user) | ||||
| 	// 更新缓存 | ||||
| 	if res.Error == nil { | ||||
| 		c.Set(types.LoginUserCache, user) | ||||
| 	} | ||||
| 	return user, res.Error | ||||
| } | ||||
							
								
								
									
										84
									
								
								api/handler/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								api/handler/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/service" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // 今日头条函数实现 | ||||
|  | ||||
| type CaptchaHandler struct { | ||||
| 	service *service.CaptchaService | ||||
| } | ||||
|  | ||||
| func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler { | ||||
| 	return &CaptchaHandler{service: s} | ||||
| } | ||||
|  | ||||
| func (h *CaptchaHandler) Get(c *gin.Context) { | ||||
| 	data, err := h.service.Get() | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, data) | ||||
| } | ||||
|  | ||||
| // Check verify the captcha data | ||||
| func (h *CaptchaHandler) Check(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Key  string `json:"key"` | ||||
| 		Dots string `json:"dots"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if h.service.Check(data) { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} else { | ||||
| 		resp.ERROR(c) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| // SlideGet 获取滑动验证图片 | ||||
| func (h *CaptchaHandler) SlideGet(c *gin.Context) { | ||||
| 	data, err := h.service.SlideGet() | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, data) | ||||
| } | ||||
|  | ||||
| // SlideCheck 滑动验证结果校验 | ||||
| func (h *CaptchaHandler) SlideCheck(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Key string `json:"key"` | ||||
| 		X   int    `json:"x"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if h.service.SlideCheck(data) { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} else { | ||||
| 		resp.ERROR(c) | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										44
									
								
								api/handler/chat_app_type_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								api/handler/chat_app_type_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatAppTypeHandler struct { | ||||
| 	BaseHandler | ||||
| } | ||||
|  | ||||
| func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler { | ||||
| 	return &ChatAppTypeHandler{BaseHandler: BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| // List 获取App类型列表 | ||||
| func (h *ChatAppTypeHandler) List(c *gin.Context) { | ||||
| 	var items []model.AppType | ||||
| 	var appTypes = make([]vo.AppType, 0) | ||||
| 	err := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range items { | ||||
| 		var appType vo.AppType | ||||
| 		err = utils.CopyObject(v, &appType) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		appType.Id = v.Id | ||||
| 		appType.CreatedAt = v.CreatedAt.Unix() | ||||
| 		appTypes = append(appTypes, appType) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, appTypes) | ||||
| } | ||||
							
								
								
									
										521
									
								
								api/handler/chat_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										521
									
								
								api/handler/chat_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,521 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/service" | ||||
| 	"geekai/service/oss" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"html/template" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatHandler struct { | ||||
| 	BaseHandler | ||||
| 	redis          *redis.Client | ||||
| 	uploadManager  *oss.UploaderManager | ||||
| 	licenseService *service.LicenseService | ||||
| 	ReqCancelFunc  *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function | ||||
| 	ChatContexts   *types.LMap[string, []interface{}]      // 聊天上下文 Map [chatId] => []Message | ||||
| 	userService    *service.UserService | ||||
| } | ||||
|  | ||||
| func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService) *ChatHandler { | ||||
| 	return &ChatHandler{ | ||||
| 		BaseHandler:    BaseHandler{App: app, DB: db}, | ||||
| 		redis:          redis, | ||||
| 		uploadManager:  manager, | ||||
| 		licenseService: licenseService, | ||||
| 		ReqCancelFunc:  types.NewLMap[string, context.CancelFunc](), | ||||
| 		ChatContexts:   types.NewLMap[string, []interface{}](), | ||||
| 		userService:    userService, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error { | ||||
| 	if !h.App.Debug { | ||||
| 		defer func() { | ||||
| 			if r := recover(); r != nil { | ||||
| 				logger.Error("Recover message from error: ", r) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
|  | ||||
| 	var user model.User | ||||
| 	res := h.DB.Model(&model.User{}).First(&user, session.UserId) | ||||
| 	if res.Error != nil { | ||||
| 		return errors.New("未授权用户,您正在进行非法操作!") | ||||
| 	} | ||||
| 	var userVo vo.User | ||||
| 	err := utils.CopyObject(user, &userVo) | ||||
| 	userVo.Id = user.Id | ||||
| 	if err != nil { | ||||
| 		return errors.New("User 对象转换失败," + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Status == false { | ||||
| 		return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!") | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Power < session.Model.Power { | ||||
| 		return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power) | ||||
| 	} | ||||
|  | ||||
| 	if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() { | ||||
| 		return errors.New("您的账号已经过期,请联系管理员!") | ||||
| 	} | ||||
|  | ||||
| 	// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度 | ||||
| 	promptTokens, err := utils.CalcTokens(prompt, session.Model.Value) | ||||
| 	if promptTokens > session.Model.MaxContext { | ||||
|  | ||||
| 		return errors.New("对话内容超出了当前模型允许的最大上下文长度!") | ||||
| 	} | ||||
|  | ||||
| 	var req = types.ApiRequest{ | ||||
| 		Model: session.Model.Value, | ||||
| 	} | ||||
| 	// 兼容 GPT-O1 模型 | ||||
| 	if strings.HasPrefix(session.Model.Value, "o1-") { | ||||
| 		utils.SendChunkMsg(ws, "AI 正在思考...\n") | ||||
| 		req.Stream = false | ||||
| 		session.Start = time.Now().Unix() | ||||
| 	} else { | ||||
| 		req.MaxTokens = session.Model.MaxTokens | ||||
| 		req.Temperature = session.Model.Temperature | ||||
| 		req.Stream = session.Stream | ||||
| 	} | ||||
|  | ||||
| 	if len(session.Tools) > 0 && !strings.HasPrefix(session.Model.Value, "o1-") { | ||||
| 		var items []model.Function | ||||
| 		res = h.DB.Where("enabled", true).Where("id IN ?", session.Tools).Find(&items) | ||||
| 		if res.Error == nil { | ||||
| 			var tools = make([]types.Tool, 0) | ||||
| 			for _, v := range items { | ||||
| 				var parameters map[string]interface{} | ||||
| 				err = utils.JsonDecode(v.Parameters, ¶meters) | ||||
| 				if err != nil { | ||||
| 					continue | ||||
| 				} | ||||
| 				tool := types.Tool{ | ||||
| 					Type: "function", | ||||
| 					Function: types.Function{ | ||||
| 						Name:        v.Name, | ||||
| 						Description: v.Description, | ||||
| 						Parameters:  parameters, | ||||
| 					}, | ||||
| 				} | ||||
| 				if v, ok := parameters["required"]; v == nil || !ok { | ||||
| 					tool.Function.Parameters["required"] = []string{} | ||||
| 				} | ||||
| 				tools = append(tools, tool) | ||||
| 			} | ||||
|  | ||||
| 			if len(tools) > 0 { | ||||
| 				req.Tools = tools | ||||
| 				req.ToolChoice = "auto" | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 加载聊天上下文 | ||||
| 	chatCtx := make([]interface{}, 0) | ||||
| 	messages := make([]interface{}, 0) | ||||
| 	if h.App.SysConfig.EnableContext { | ||||
| 		if h.ChatContexts.Has(session.ChatId) { | ||||
| 			messages = h.ChatContexts.Get(session.ChatId) | ||||
| 		} else { | ||||
| 			_ = utils.JsonDecode(role.Context, &messages) | ||||
| 			if h.App.SysConfig.ContextDeep > 0 { | ||||
| 				var historyMessages []model.ChatMessage | ||||
| 				res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages) | ||||
| 				if res.Error == nil { | ||||
| 					for i := len(historyMessages) - 1; i >= 0; i-- { | ||||
| 						msg := historyMessages[i] | ||||
| 						ms := types.Message{Role: "user", Content: msg.Content} | ||||
| 						if msg.Type == types.ReplyMsg { | ||||
| 							ms.Role = "assistant" | ||||
| 						} | ||||
| 						chatCtx = append(chatCtx, ms) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// 计算当前请求的 token 总长度,确保不会超出最大上下文长度 | ||||
| 		// MaxContextLength = Response + Tool + Prompt + Context | ||||
| 		tokens := req.MaxTokens // 最大响应长度 | ||||
| 		tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model) | ||||
| 		tokens += tks + promptTokens | ||||
|  | ||||
| 		for i := len(messages) - 1; i >= 0; i-- { | ||||
| 			v := messages[i] | ||||
| 			tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model) | ||||
| 			// 上下文 token 超出了模型的最大上下文长度 | ||||
| 			if tokens+tks >= session.Model.MaxContext { | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// 上下文的深度超出了模型的最大上下文深度 | ||||
| 			if len(chatCtx) >= h.App.SysConfig.ContextDeep { | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			tokens += tks | ||||
| 			chatCtx = append(chatCtx, v) | ||||
| 		} | ||||
|  | ||||
| 		logger.Debugf("聊天上下文:%+v", chatCtx) | ||||
| 	} | ||||
| 	reqMgs := make([]interface{}, 0) | ||||
|  | ||||
| 	for i := len(chatCtx) - 1; i >= 0; i-- { | ||||
| 		reqMgs = append(reqMgs, chatCtx[i]) | ||||
| 	} | ||||
|  | ||||
| 	fullPrompt := prompt | ||||
| 	text := prompt | ||||
| 	// extract files in prompt | ||||
| 	files := utils.ExtractFileURLs(prompt) | ||||
| 	logger.Debugf("detected FILES: %+v", files) | ||||
| 	// 如果不是逆向模型,则提取文件内容 | ||||
| 	if len(files) > 0 && !(session.Model.Value == "gpt-4-all" || | ||||
| 		strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") || | ||||
| 		strings.HasSuffix(session.Model.Value, "claude-3")) { | ||||
| 		contents := make([]string, 0) | ||||
| 		var file model.File | ||||
| 		for _, v := range files { | ||||
| 			h.DB.Where("url = ?", v).First(&file) | ||||
| 			content, err := utils.ReadFileContent(v, h.App.Config.TikaHost) | ||||
| 			if err != nil { | ||||
| 				logger.Error("error with read file: ", err) | ||||
| 			} else { | ||||
| 				contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content)) | ||||
| 			} | ||||
| 			text = strings.Replace(text, v, "", 1) | ||||
| 		} | ||||
| 		if len(contents) > 0 { | ||||
| 			fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text) | ||||
| 		} | ||||
|  | ||||
| 		tokens, _ := utils.CalcTokens(fullPrompt, req.Model) | ||||
| 		if tokens > session.Model.MaxContext { | ||||
| 			return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。") | ||||
| 		} | ||||
| 	} | ||||
| 	logger.Debug("最终Prompt:", fullPrompt) | ||||
|  | ||||
| 	// extract images from prompt | ||||
| 	imgURLs := utils.ExtractImgURLs(prompt) | ||||
| 	logger.Debugf("detected IMG: %+v", imgURLs) | ||||
| 	var content interface{} | ||||
| 	if len(imgURLs) > 0 { | ||||
| 		data := make([]interface{}, 0) | ||||
| 		for _, v := range imgURLs { | ||||
| 			text = strings.Replace(text, v, "", 1) | ||||
| 			data = append(data, gin.H{ | ||||
| 				"type": "image_url", | ||||
| 				"image_url": gin.H{ | ||||
| 					"url": v, | ||||
| 				}, | ||||
| 			}) | ||||
| 		} | ||||
| 		data = append(data, gin.H{ | ||||
| 			"type": "text", | ||||
| 			"text": strings.TrimSpace(text), | ||||
| 		}) | ||||
| 		content = data | ||||
| 	} else { | ||||
| 		content = fullPrompt | ||||
| 	} | ||||
| 	req.Messages = append(reqMgs, map[string]interface{}{ | ||||
| 		"role":    "user", | ||||
| 		"content": content, | ||||
| 	}) | ||||
|  | ||||
| 	logger.Debugf("%+v", req.Messages) | ||||
|  | ||||
| 	return h.sendOpenAiMessage(req, userVo, ctx, session, role, prompt, ws) | ||||
| } | ||||
|  | ||||
| // Tokens 统计 token 数量 | ||||
| func (h *ChatHandler) Tokens(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Text   string `json:"text"` | ||||
| 		Model  string `json:"model"` | ||||
| 		ChatId string `json:"chat_id"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文) | ||||
| 	//if data.Text == "" && data.ChatId != "" { | ||||
| 	//	var item model.ChatMessage | ||||
| 	//	userId, _ := c.Get(types.LoginUserID) | ||||
| 	//	res := h.DB.Where("user_id = ?", userId).Where("chat_id = ?", data.ChatId).Last(&item) | ||||
| 	//	if res.Error != nil { | ||||
| 	//		resp.ERROR(c, res.Error.Error()) | ||||
| 	//		return | ||||
| 	//	} | ||||
| 	//	resp.SUCCESS(c, item.Tokens) | ||||
| 	//	return | ||||
| 	//} | ||||
|  | ||||
| 	tokens, err := utils.CalcTokens(data.Text, data.Model) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, tokens) | ||||
| } | ||||
|  | ||||
| func getTotalTokens(req types.ApiRequest) int { | ||||
| 	encode := utils.JsonEncode(req.Messages) | ||||
| 	var items []map[string]interface{} | ||||
| 	err := utils.JsonDecode(encode, &items) | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	tokens := 0 | ||||
| 	for _, item := range items { | ||||
| 		content, ok := item["content"] | ||||
| 		if ok && !utils.IsEmptyValue(content) { | ||||
| 			t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model) | ||||
| 			if err == nil { | ||||
| 				tokens += t | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return tokens | ||||
| } | ||||
|  | ||||
| // StopGenerate 停止生成 | ||||
| func (h *ChatHandler) StopGenerate(c *gin.Context) { | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	if h.ReqCancelFunc.Has(sessionId) { | ||||
| 		h.ReqCancelFunc.Get(sessionId)() | ||||
| 		h.ReqCancelFunc.Delete(sessionId) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // 发送请求到 OpenAI 服务器 | ||||
| // useOwnApiKey: 是否使用了用户自己的 API KEY | ||||
| func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) { | ||||
| 	// if the chat model bind a KEY, use it directly | ||||
| 	if session.Model.KeyId > 0 { | ||||
| 		h.DB.Where("id", session.Model.KeyId).Find(apiKey) | ||||
| 	} | ||||
| 	// use the last unused key | ||||
| 	if apiKey.Id == 0 { | ||||
| 		h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey) | ||||
| 	} | ||||
| 	if apiKey.Id == 0 { | ||||
| 		return nil, errors.New("no available key, please import key") | ||||
| 	} | ||||
|  | ||||
| 	// ONLY allow apiURL in blank list | ||||
| 	err := h.licenseService.IsValidApiURL(apiKey.ApiURL) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	logger.Debugf("对话请求消息体:%+v", req) | ||||
|  | ||||
| 	apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) | ||||
| 	// 创建 HttpClient 请求对象 | ||||
| 	var client *http.Client | ||||
| 	requestBody, err := json.Marshal(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	request = request.WithContext(ctx) | ||||
| 	request.Header.Set("Content-Type", "application/json") | ||||
| 	if len(apiKey.ProxyURL) > 5 { // 使用代理 | ||||
| 		proxy, _ := url.Parse(apiKey.ProxyURL) | ||||
| 		client = &http.Client{ | ||||
| 			Transport: &http.Transport{ | ||||
| 				Proxy: http.ProxyURL(proxy), | ||||
| 			}, | ||||
| 		} | ||||
| 	} else { | ||||
| 		client = http.DefaultClient | ||||
| 	} | ||||
| 	logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model) | ||||
| 	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value)) | ||||
| 	// 更新API KEY 最后使用时间 | ||||
| 	h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix()) | ||||
| 	return client.Do(request) | ||||
| } | ||||
|  | ||||
| // 扣减用户算力 | ||||
| func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) { | ||||
| 	power := 1 | ||||
| 	if session.Model.Power > 0 { | ||||
| 		power = session.Model.Power | ||||
| 	} | ||||
|  | ||||
| 	err := h.userService.DecreasePower(int(userVo.Id), power, model.PowerLog{ | ||||
| 		Type:   types.PowerConsume, | ||||
| 		Model:  session.Model.Value, | ||||
| 		Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *ChatHandler) saveChatHistory( | ||||
| 	req types.ApiRequest, | ||||
| 	usage Usage, | ||||
| 	message types.Message, | ||||
| 	session *types.ChatSession, | ||||
| 	role model.ChatRole, | ||||
| 	userVo vo.User, | ||||
| 	promptCreatedAt time.Time, | ||||
| 	replyCreatedAt time.Time) { | ||||
|  | ||||
| 	// 更新上下文消息 | ||||
| 	if h.App.SysConfig.EnableContext { | ||||
| 		chatCtx := req.Messages            // 提问消息 | ||||
| 		chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 		h.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 	} | ||||
|  | ||||
| 	// 追加聊天记录 | ||||
| 	// for prompt | ||||
| 	var promptTokens, replyTokens, totalTokens int | ||||
| 	if usage.PromptTokens > 0 { | ||||
| 		promptTokens = usage.PromptTokens | ||||
| 	} else { | ||||
| 		promptTokens, _ = utils.CalcTokens(usage.Content, req.Model) | ||||
| 	} | ||||
|  | ||||
| 	historyUserMsg := model.ChatMessage{ | ||||
| 		UserId:      userVo.Id, | ||||
| 		ChatId:      session.ChatId, | ||||
| 		RoleId:      role.Id, | ||||
| 		Type:        types.PromptMsg, | ||||
| 		Icon:        userVo.Avatar, | ||||
| 		Content:     template.HTMLEscapeString(usage.Prompt), | ||||
| 		Tokens:      promptTokens, | ||||
| 		TotalTokens: promptTokens, | ||||
| 		UseContext:  true, | ||||
| 		Model:       req.Model, | ||||
| 	} | ||||
| 	historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 	historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 	err := h.DB.Save(&historyUserMsg).Error | ||||
| 	if err != nil { | ||||
| 		logger.Error("failed to save prompt history message: ", err) | ||||
| 	} | ||||
|  | ||||
| 	// for reply | ||||
| 	// 计算本次对话消耗的总 token 数量 | ||||
| 	if usage.CompletionTokens > 0 { | ||||
| 		replyTokens = usage.CompletionTokens | ||||
| 		totalTokens = usage.TotalTokens | ||||
| 	} else { | ||||
| 		replyTokens, _ = utils.CalcTokens(message.Content, req.Model) | ||||
| 		totalTokens = replyTokens + getTotalTokens(req) | ||||
| 	} | ||||
| 	historyReplyMsg := model.ChatMessage{ | ||||
| 		UserId:      userVo.Id, | ||||
| 		ChatId:      session.ChatId, | ||||
| 		RoleId:      role.Id, | ||||
| 		Type:        types.ReplyMsg, | ||||
| 		Icon:        role.Icon, | ||||
| 		Content:     usage.Content, | ||||
| 		Tokens:      replyTokens, | ||||
| 		TotalTokens: totalTokens, | ||||
| 		UseContext:  true, | ||||
| 		Model:       req.Model, | ||||
| 	} | ||||
| 	historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 	historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 	err = h.DB.Create(&historyReplyMsg).Error | ||||
| 	if err != nil { | ||||
| 		logger.Error("failed to save reply history message: ", err) | ||||
| 	} | ||||
|  | ||||
| 	// 更新用户算力 | ||||
| 	if session.Model.Power > 0 { | ||||
| 		h.subUserPower(userVo, session, promptTokens, replyTokens) | ||||
| 	} | ||||
| 	// 保存当前会话 | ||||
| 	var chatItem model.ChatItem | ||||
| 	err = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem).Error | ||||
| 	if err != nil { | ||||
| 		chatItem.ChatId = session.ChatId | ||||
| 		chatItem.UserId = userVo.Id | ||||
| 		chatItem.RoleId = role.Id | ||||
| 		chatItem.ModelId = session.Model.Id | ||||
| 		if utf8.RuneCountInString(usage.Prompt) > 30 { | ||||
| 			chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..." | ||||
| 		} else { | ||||
| 			chatItem.Title = usage.Prompt | ||||
| 		} | ||||
| 		chatItem.Model = req.Model | ||||
| 		err = h.DB.Create(&chatItem).Error | ||||
| 		if err != nil { | ||||
| 			logger.Error("failed to save chat item: ", err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 将AI回复消息中生成的图片链接下载到本地 | ||||
| func (h *ChatHandler) extractImgUrl(text string) string { | ||||
| 	pattern := `!\[([^\]]*)]\(([^)]+)\)` | ||||
| 	re := regexp.MustCompile(pattern) | ||||
| 	matches := re.FindAllStringSubmatch(text, -1) | ||||
|  | ||||
| 	// 下载图片并替换链接地址 | ||||
| 	for _, match := range matches { | ||||
| 		imageURL := match[2] | ||||
| 		logger.Debug(imageURL) | ||||
| 		// 对于相同地址的图片,已经被替换了,就不再重复下载了 | ||||
| 		if !strings.Contains(text, imageURL) { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false) | ||||
| 		if err != nil { | ||||
| 			logger.Error("error with download image: ", err) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		text = strings.ReplaceAll(text, imageURL, newImgURL) | ||||
| 	} | ||||
| 	return text | ||||
| } | ||||
							
								
								
									
										220
									
								
								api/handler/chat_item_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								api/handler/chat_item_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,220 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // List 获取会话列表 | ||||
| func (h *ChatHandler) List(c *gin.Context) { | ||||
| 	if !h.IsLogin(c) { | ||||
| 		resp.SUCCESS(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	var items = make([]vo.ChatItem, 0) | ||||
| 	var chats []model.ChatItem | ||||
| 	h.DB.Where("user_id", userId).Order("id DESC").Find(&chats) | ||||
| 	if len(chats) == 0 { | ||||
| 		resp.SUCCESS(c, items) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var roleIds = make([]uint, 0) | ||||
| 	var modelValues = make([]string, 0) | ||||
| 	for _, chat := range chats { | ||||
| 		roleIds = append(roleIds, chat.RoleId) | ||||
| 		modelValues = append(modelValues, chat.Model) | ||||
| 	} | ||||
|  | ||||
| 	var roles []model.ChatRole | ||||
| 	var models []model.ChatModel | ||||
| 	roleMap := make(map[uint]model.ChatRole) | ||||
| 	modelMap := make(map[string]model.ChatModel) | ||||
| 	h.DB.Where("id IN ?", roleIds).Find(&roles) | ||||
| 	h.DB.Where("value IN ?", modelValues).Find(&models) | ||||
| 	for _, role := range roles { | ||||
| 		roleMap[role.Id] = role | ||||
| 	} | ||||
| 	for _, m := range models { | ||||
| 		modelMap[m.Value] = m | ||||
| 	} | ||||
| 	for _, chat := range chats { | ||||
| 		var item vo.ChatItem | ||||
| 		err := utils.CopyObject(chat, &item) | ||||
| 		if err == nil { | ||||
| 			item.Id = chat.Id | ||||
| 			item.Icon = roleMap[chat.RoleId].Icon | ||||
| 			item.ModelId = modelMap[chat.Model].Id | ||||
| 			items = append(items, item) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, items) | ||||
| } | ||||
|  | ||||
| // Update 更新会话标题 | ||||
| func (h *ChatHandler) Update(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		ChatId string `json:"chat_id"` | ||||
| 		Title  string `json:"title"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	res := h.DB.Model(&model.ChatItem{}).Where("chat_id = ?", data.ChatId).UpdateColumn("title", data.Title) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // Clear 清空所有聊天记录 | ||||
| func (h *ChatHandler) Clear(c *gin.Context) { | ||||
| 	// 获取当前登录用户所有的聊天会话 | ||||
| 	user, err := h.GetLoginUser(c) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chats []model.ChatItem | ||||
| 	res := h.DB.Where("user_id = ?", user.Id).Find(&chats) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No chats found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chatIds = make([]string, 0) | ||||
| 	for _, chat := range chats { | ||||
| 		chatIds = append(chatIds, chat.ChatId) | ||||
| 		// 清空会话上下文 | ||||
| 		h.ChatContexts.Delete(chat.ChatId) | ||||
| 	} | ||||
| 	err = h.DB.Transaction(func(tx *gorm.DB) error { | ||||
| 		res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{}) | ||||
| 		if res.Error != nil { | ||||
| 			return res.Error | ||||
| 		} | ||||
|  | ||||
| 		res = h.DB.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.ChatMessage{}) | ||||
| 		if res.Error != nil { | ||||
| 			return res.Error | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error with delete chats: %+v", err) | ||||
| 		resp.ERROR(c, "Failed to remove chat from database.") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // History 获取聊天历史记录 | ||||
| func (h *ChatHandler) History(c *gin.Context) { | ||||
| 	chatId := c.Query("chat_id") // 会话 ID | ||||
| 	var items []model.ChatMessage | ||||
| 	var messages = make([]vo.HistoryMessage, 0) | ||||
| 	res := h.DB.Where("chat_id = ?", chatId).Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No history message") | ||||
| 		return | ||||
| 	} else { | ||||
| 		for _, item := range items { | ||||
| 			var v vo.HistoryMessage | ||||
| 			err := utils.CopyObject(item, &v) | ||||
| 			v.CreatedAt = item.CreatedAt.Unix() | ||||
| 			v.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 			if err == nil { | ||||
| 				messages = append(messages, v) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, messages) | ||||
| } | ||||
|  | ||||
| // Remove 删除会话 | ||||
| func (h *ChatHandler) Remove(c *gin.Context) { | ||||
| 	chatId := h.GetTrim(c, "chat_id") | ||||
| 	if chatId == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	user, err := h.GetLoginUser(c) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res := h.DB.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 删除当前会话的聊天记录 | ||||
| 	res = h.DB.Where("user_id = ? AND chat_id =?", user.Id, chatId).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to remove chat from database.") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// TODO: 是否要删除 MidJourney 绘画记录和图片文件? | ||||
|  | ||||
| 	// 清空会话上下文 | ||||
| 	h.ChatContexts.Delete(chatId) | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // Detail 对话详情,用户导出对话 | ||||
| func (h *ChatHandler) Detail(c *gin.Context) { | ||||
| 	chatId := h.GetTrim(c, "chat_id") | ||||
| 	if utils.IsEmptyValue(chatId) { | ||||
| 		resp.ERROR(c, "Invalid chatId") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chatItem model.ChatItem | ||||
| 	res := h.DB.Where("chat_id = ?", chatId).First(&chatItem) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No chat found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 填充角色名称 | ||||
| 	var role model.ChatRole | ||||
| 	res = h.DB.Where("id", chatItem.RoleId).First(&role) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Role not found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chatItemVo vo.ChatItem | ||||
| 	err := utils.CopyObject(chatItem, &chatItemVo) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	chatItemVo.RoleName = role.Name | ||||
| 	resp.SUCCESS(c, chatItemVo) | ||||
| } | ||||
							
								
								
									
										67
									
								
								api/handler/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								api/handler/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatModelHandler struct { | ||||
| 	BaseHandler | ||||
| } | ||||
|  | ||||
| func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler { | ||||
| 	return &ChatModelHandler{BaseHandler: BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| // List 模型列表 | ||||
| func (h *ChatModelHandler) List(c *gin.Context) { | ||||
| 	var items []model.ChatModel | ||||
| 	var chatModels = make([]vo.ChatModel, 0) | ||||
| 	session := h.DB.Session(&gorm.Session{}).Where("enabled", true) | ||||
| 	t := c.Query("type") | ||||
| 	if t != "" { | ||||
| 		session = session.Where("type", t) | ||||
| 	} | ||||
|  | ||||
| 	session = session.Where("open", true) | ||||
| 	if h.IsLogin(c) { | ||||
| 		user, _ := h.GetLoginUser(c) | ||||
| 		var models []int | ||||
| 		err := utils.JsonDecode(user.ChatModels, &models) | ||||
| 		// 查询用户有权限访问的模型以及所有开放的模型 | ||||
| 		if err == nil { | ||||
| 			session = session.Or("id IN ?", models) | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	res := session.Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var cm vo.ChatModel | ||||
| 			err := utils.CopyObject(item, &cm) | ||||
| 			if err == nil { | ||||
| 				cm.Id = item.Id | ||||
| 				cm.CreatedAt = item.CreatedAt.Unix() | ||||
| 				cm.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				chatModels = append(chatModels, cm) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, chatModels) | ||||
| } | ||||
							
								
								
									
										122
									
								
								api/handler/chat_role_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								api/handler/chat_role_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatRoleHandler struct { | ||||
| 	BaseHandler | ||||
| } | ||||
|  | ||||
| func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler { | ||||
| 	return &ChatRoleHandler{BaseHandler: BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| // List 获取用户聊天应用列表 | ||||
| func (h *ChatRoleHandler) List(c *gin.Context) { | ||||
| 	tid := h.GetInt(c, "tid", 0) | ||||
| 	var roles []model.ChatRole | ||||
| 	session := h.DB.Where("enable", true) | ||||
| 	if tid > 0 { | ||||
| 		session = session.Where("tid", tid) | ||||
| 	} | ||||
| 	err := session.Order("sort_num ASC").Find(&roles).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var roleVos = make([]vo.ChatRole, 0) | ||||
| 	for _, r := range roles { | ||||
| 		var v vo.ChatRole | ||||
| 		err := utils.CopyObject(r, &v) | ||||
| 		if err == nil { | ||||
| 			v.Id = r.Id | ||||
| 			roleVos = append(roleVos, v) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, roleVos) | ||||
| } | ||||
|  | ||||
| // ListByUser 获取用户添加的角色列表 | ||||
| func (h *ChatRoleHandler) ListByUser(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	var roles []model.ChatRole | ||||
| 	session := h.DB.Where("enable", true) | ||||
| 	// 如果用户没登录,则获取所有角色 | ||||
| 	if userId > 0 { | ||||
| 		var user model.User | ||||
| 		h.DB.First(&user, userId) | ||||
| 		var roleKeys []string | ||||
| 		err := utils.JsonDecode(user.ChatRoles, &roleKeys) | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, "角色解析失败!") | ||||
| 			return | ||||
| 		} | ||||
| 		// 保证用户至少有一个角色可用 | ||||
| 		if len(roleKeys) > 0 { | ||||
| 			session = session.Where("marker IN ?", roleKeys) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if id > 0 { | ||||
| 		session = session.Or("id", id) | ||||
| 	} | ||||
| 	res := session.Order("sort_num ASC").Find(&roles) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var roleVos = make([]vo.ChatRole, 0) | ||||
| 	for _, r := range roles { | ||||
| 		var v vo.ChatRole | ||||
| 		err := utils.CopyObject(r, &v) | ||||
| 		if err == nil { | ||||
| 			v.Id = r.Id | ||||
| 			roleVos = append(roleVos, v) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, roleVos) | ||||
| } | ||||
|  | ||||
| // UpdateRole 更新用户聊天角色 | ||||
| func (h *ChatRoleHandler) UpdateRole(c *gin.Context) { | ||||
| 	user, err := h.GetLoginUser(c) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var data struct { | ||||
| 		Keys []string `json:"keys"` | ||||
| 	} | ||||
| 	if err = c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										54
									
								
								api/handler/config_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								api/handler/config_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/service" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ConfigHandler struct { | ||||
| 	BaseHandler | ||||
| 	licenseService *service.LicenseService | ||||
| } | ||||
|  | ||||
| func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler { | ||||
| 	return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService} | ||||
| } | ||||
|  | ||||
| // Get 获取指定的系统配置 | ||||
| func (h *ConfigHandler) Get(c *gin.Context) { | ||||
| 	key := c.Query("key") | ||||
| 	var config model.Config | ||||
| 	res := h.DB.Where("marker", key).First(&config) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var value map[string]interface{} | ||||
| 	err := utils.JsonDecode(config.Config, &value) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, value) | ||||
| } | ||||
|  | ||||
| // License 获取 License 配置 | ||||
| func (h *ConfigHandler) License(c *gin.Context) { | ||||
| 	license := h.licenseService.GetLicense() | ||||
| 	resp.SUCCESS(c, license.Configs) | ||||
| } | ||||
							
								
								
									
										212
									
								
								api/handler/dalle_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								api/handler/dalle_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/service" | ||||
| 	"geekai/service/dalle" | ||||
| 	"geekai/service/oss" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type DallJobHandler struct { | ||||
| 	BaseHandler | ||||
| 	redis       *redis.Client | ||||
| 	dallService *dalle.Service | ||||
| 	uploader    *oss.UploaderManager | ||||
| 	userService *service.UserService | ||||
| } | ||||
|  | ||||
| func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService) *DallJobHandler { | ||||
| 	return &DallJobHandler{ | ||||
| 		dallService: service, | ||||
| 		uploader:    manager, | ||||
| 		userService: userService, | ||||
| 		BaseHandler: BaseHandler{ | ||||
| 			App: app, | ||||
| 			DB:  db, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *DallJobHandler) preCheck(c *gin.Context) bool { | ||||
| 	user, err := h.GetLoginUser(c) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return false | ||||
| 	} | ||||
| 	if user.Power < h.App.SysConfig.DallPower { | ||||
| 		resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!") | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return true | ||||
|  | ||||
| } | ||||
|  | ||||
| // Image 创建一个绘画任务 | ||||
| func (h *DallJobHandler) Image(c *gin.Context) { | ||||
| 	if !h.preCheck(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var data types.DallTask | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	task := types.DallTask{ | ||||
| 		ClientId:         data.ClientId, | ||||
| 		UserId:           uint(userId), | ||||
| 		Prompt:           data.Prompt, | ||||
| 		Quality:          data.Quality, | ||||
| 		Size:             data.Size, | ||||
| 		Style:            data.Style, | ||||
| 		Power:            h.App.SysConfig.DallPower, | ||||
| 		TranslateModelId: h.App.SysConfig.TranslateModelId, | ||||
| 	} | ||||
| 	job := model.DallJob{ | ||||
| 		UserId:   uint(userId), | ||||
| 		Prompt:   data.Prompt, | ||||
| 		Power:    task.Power, | ||||
| 		TaskInfo: utils.JsonEncode(task), | ||||
| 	} | ||||
| 	res := h.DB.Create(&job) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "error with save job: "+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	task.Id = job.Id | ||||
| 	h.dallService.PushTask(task) | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // ImgWall 照片墙 | ||||
| func (h *DallJobHandler) ImgWall(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 0) | ||||
| 	pageSize := h.GetInt(c, "page_size", 0) | ||||
| 	err, jobs := h.getData(true, 0, page, pageSize, true) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, jobs) | ||||
| } | ||||
|  | ||||
| // JobList 获取 SD 任务列表 | ||||
| func (h *DallJobHandler) JobList(c *gin.Context) { | ||||
| 	finish := h.GetBool(c, "finish") | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	page := h.GetInt(c, "page", 0) | ||||
| 	pageSize := h.GetInt(c, "page_size", 0) | ||||
| 	publish := h.GetBool(c, "publish") | ||||
|  | ||||
| 	err, jobs := h.getData(finish, userId, page, pageSize, publish) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, jobs) | ||||
| } | ||||
|  | ||||
| // JobList 获取任务列表 | ||||
| func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) { | ||||
|  | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if finish { | ||||
| 		session = session.Where("progress >= ?", 100).Order("id DESC") | ||||
| 	} else { | ||||
| 		session = session.Where("progress < ?", 100).Order("id ASC") | ||||
| 	} | ||||
| 	if userId > 0 { | ||||
| 		session = session.Where("user_id = ?", userId) | ||||
| 	} | ||||
| 	if publish { | ||||
| 		session = session.Where("publish", publish) | ||||
| 	} | ||||
| 	if page > 0 && pageSize > 0 { | ||||
| 		offset := (page - 1) * pageSize | ||||
| 		session = session.Offset(offset).Limit(pageSize) | ||||
| 	} | ||||
| 	// 统计总数 | ||||
| 	var total int64 | ||||
| 	session.Model(&model.DallJob{}).Count(&total) | ||||
|  | ||||
| 	var items []model.DallJob | ||||
| 	res := session.Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		return res.Error, vo.Page{} | ||||
| 	} | ||||
|  | ||||
| 	var jobs = make([]vo.DallJob, 0) | ||||
| 	for _, item := range items { | ||||
| 		var job vo.DallJob | ||||
| 		err := utils.CopyObject(item, &job) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		jobs = append(jobs, job) | ||||
| 	} | ||||
|  | ||||
| 	return nil, vo.NewPage(total, page, pageSize, jobs) | ||||
| } | ||||
|  | ||||
| // Remove remove task image | ||||
| func (h *DallJobHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	var job model.DallJob | ||||
| 	if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil { | ||||
| 		resp.ERROR(c, "记录不存在") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 删除任务 | ||||
| 	err := h.DB.Delete(&job).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// remove image | ||||
| 	err = h.uploader.GetUploadHandler().Delete(job.ImgURL) | ||||
| 	if err != nil { | ||||
| 		logger.Error("remove image failed: ", err) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Publish 发布/取消发布图片到画廊显示 | ||||
| func (h *DallJobHandler) Publish(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享 | ||||
|  | ||||
| 	err := h.DB.Model(&model.DallJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										254
									
								
								api/handler/function_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								api/handler/function_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,254 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/service/dalle" | ||||
| 	"geekai/service/oss" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type FunctionHandler struct { | ||||
| 	BaseHandler | ||||
| 	config        types.ApiConfig | ||||
| 	uploadManager *oss.UploaderManager | ||||
| 	dallService   *dalle.Service | ||||
| } | ||||
|  | ||||
| func NewFunctionHandler( | ||||
| 	server *core.AppServer, | ||||
| 	db *gorm.DB, | ||||
| 	config *types.AppConfig, | ||||
| 	manager *oss.UploaderManager, | ||||
| 	dallService *dalle.Service) *FunctionHandler { | ||||
| 	return &FunctionHandler{ | ||||
| 		BaseHandler: BaseHandler{ | ||||
| 			App: server, | ||||
| 			DB:  db, | ||||
| 		}, | ||||
| 		config:        config.ApiConfig, | ||||
| 		uploadManager: manager, | ||||
| 		dallService:   dallService, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type resVo struct { | ||||
| 	Code    types.BizCode `json:"code"` | ||||
| 	Message string        `json:"message"` | ||||
| 	Data    struct { | ||||
| 		Title     string     `json:"title"` | ||||
| 		UpdatedAt string     `json:"updated_at"` | ||||
| 		Items     []dataItem `json:"items"` | ||||
| 	} `json:"data"` | ||||
| } | ||||
|  | ||||
| type dataItem struct { | ||||
| 	Title  string `json:"title"` | ||||
| 	Url    string `json:"url"` | ||||
| 	Remark string `json:"remark"` | ||||
| } | ||||
|  | ||||
| // check authorization | ||||
| func (h *FunctionHandler) checkAuth(c *gin.Context) error { | ||||
| 	tokenString := c.GetHeader(types.UserAuthHeader) | ||||
| 	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { | ||||
| 		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||||
| 			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | ||||
| 		} | ||||
|  | ||||
| 		return []byte(h.App.Config.Session.SecretKey), nil | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error with parse auth token: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	claims, ok := token.Claims.(jwt.MapClaims) | ||||
| 	if !ok || !token.Valid { | ||||
| 		return errors.New("token is invalid") | ||||
| 	} | ||||
|  | ||||
| 	expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0) | ||||
| 	if expr > 0 && int64(expr) < time.Now().Unix() { | ||||
| 		return errors.New("token is expired") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // WeiBo 微博热搜 | ||||
| func (h *FunctionHandler) WeiBo(c *gin.Context) { | ||||
| 	if err := h.checkAuth(c); err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if h.config.Token == "" { | ||||
| 		resp.ERROR(c, "无效的 API Token") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	url := fmt.Sprintf("%s/api/weibo/fetch", h.config.ApiURL) | ||||
| 	var res resVo | ||||
| 	r, err := req.C().R(). | ||||
| 		SetHeader("AppId", h.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, fmt.Sprintf("%v", err)) | ||||
| 		return | ||||
| 	} | ||||
| 	if r.IsErrorState() { | ||||
| 		resp.ERROR(c, fmt.Sprintf("error http code status: %v", r.Status)) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		resp.ERROR(c, res.Message) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	builder := make([]string, 0) | ||||
| 	builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt)) | ||||
| 	for i, v := range res.Data.Items { | ||||
| 		builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark)) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, strings.Join(builder, "\n\n")) | ||||
| } | ||||
|  | ||||
| // ZaoBao 今日早报 | ||||
| func (h *FunctionHandler) ZaoBao(c *gin.Context) { | ||||
| 	if err := h.checkAuth(c); err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if h.config.Token == "" { | ||||
| 		resp.ERROR(c, "无效的 API Token") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	url := fmt.Sprintf("%s/api/zaobao/fetch", h.config.ApiURL) | ||||
| 	var res resVo | ||||
| 	r, err := req.C().R(). | ||||
| 		SetHeader("AppId", h.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		resp.ERROR(c, fmt.Sprintf("%v%v", err, r.Err)) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		resp.ERROR(c, res.Message) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	builder := make([]string, 0) | ||||
| 	builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt)) | ||||
| 	for _, v := range res.Data.Items { | ||||
| 		builder = append(builder, v.Title) | ||||
| 	} | ||||
| 	builder = append(builder, fmt.Sprintf("%s", res.Data.Title)) | ||||
| 	resp.SUCCESS(c, strings.Join(builder, "\n\n")) | ||||
| } | ||||
|  | ||||
| // Dall3 DallE3 AI 绘图 | ||||
| func (h *FunctionHandler) Dall3(c *gin.Context) { | ||||
| 	if err := h.checkAuth(c); err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var params map[string]interface{} | ||||
| 	if err := c.ShouldBindJSON(¶ms); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	logger.Debugf("绘画参数:%+v", params) | ||||
| 	var user model.User | ||||
| 	res := h.DB.Where("id = ?", params["user_id"]).First(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "当前用户不存在!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if user.Power < h.App.SysConfig.DallPower { | ||||
| 		resp.ERROR(c, "创建 DALL-E 绘图任务失败,算力不足") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// create dall task | ||||
| 	prompt := utils.InterfaceToString(params["prompt"]) | ||||
| 	job := model.DallJob{ | ||||
| 		UserId: user.Id, | ||||
| 		Prompt: prompt, | ||||
| 		Power:  h.App.SysConfig.DallPower, | ||||
| 	} | ||||
| 	res = h.DB.Create(&job) | ||||
|  | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	content, err := h.dallService.Image(types.DallTask{ | ||||
| 		Id:      job.Id, | ||||
| 		UserId:  user.Id, | ||||
| 		Prompt:  job.Prompt, | ||||
| 		N:       1, | ||||
| 		Quality: "standard", | ||||
| 		Size:    "1024x1024", | ||||
| 		Style:   "vivid", | ||||
| 		Power:   job.Power, | ||||
| 	}, true) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "任务执行失败:"+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, content) | ||||
| } | ||||
|  | ||||
| // List 获取所有的工具函数列表 | ||||
| func (h *FunctionHandler) List(c *gin.Context) { | ||||
| 	var items []model.Function | ||||
| 	err := h.DB.Where("enabled", true).Find(&items).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tools := make([]vo.Function, 0) | ||||
| 	for _, v := range items { | ||||
| 		var f vo.Function | ||||
| 		err = utils.CopyObject(v, &f) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		f.Action = "" | ||||
| 		f.Token = "" | ||||
| 		tools = append(tools, f) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, tools) | ||||
| } | ||||
							
								
								
									
										92
									
								
								api/handler/invite_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								api/handler/invite_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // InviteHandler 用户邀请 | ||||
| type InviteHandler struct { | ||||
| 	BaseHandler | ||||
| } | ||||
|  | ||||
| func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler { | ||||
| 	return &InviteHandler{BaseHandler: BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| // Code 获取当前用户邀请码 | ||||
| func (h *InviteHandler) Code(c *gin.Context) { | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	var inviteCode model.InviteCode | ||||
| 	res := h.DB.Where("user_id = ?", userId).First(&inviteCode) | ||||
| 	// 如果邀请码不存在,则创建一个 | ||||
| 	if res.Error != nil { | ||||
| 		code := strings.ToUpper(utils.RandString(8)) | ||||
| 		for { | ||||
| 			res = h.DB.Where("code = ?", code).First(&inviteCode) | ||||
| 			if res.Error != nil { // 不存在相同的邀请码则退出 | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		inviteCode.UserId = userId | ||||
| 		inviteCode.Code = code | ||||
| 		h.DB.Create(&inviteCode) | ||||
| 	} | ||||
|  | ||||
| 	var codeVo vo.InviteCode | ||||
| 	err := utils.CopyObject(inviteCode, &codeVo) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "拷贝对象失败") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, codeVo) | ||||
| } | ||||
|  | ||||
| // List Log 用户邀请记录 | ||||
| func (h *InviteHandler) List(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 1) | ||||
| 	pageSize := h.GetInt(c, "page_size", 20) | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	session := h.DB.Session(&gorm.Session{}).Where("inviter_id = ?", userId) | ||||
| 	var total int64 | ||||
| 	session.Model(&model.InviteLog{}).Count(&total) | ||||
| 	var items []model.InviteLog | ||||
| 	var list = make([]vo.InviteLog, 0) | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var v vo.InviteLog | ||||
| 			err := utils.CopyObject(item, &v) | ||||
| 			if err == nil { | ||||
| 				v.Id = item.Id | ||||
| 				v.CreatedAt = item.CreatedAt.Unix() | ||||
| 				list = append(list, v) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list)) | ||||
| } | ||||
|  | ||||
| // Hits 访问邀请码 | ||||
| func (h *InviteHandler) Hits(c *gin.Context) { | ||||
| 	code := c.Query("code") | ||||
| 	h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1)) | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										110
									
								
								api/handler/markmap_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								api/handler/markmap_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/service" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // MarkMapHandler 生成思维导图 | ||||
| type MarkMapHandler struct { | ||||
| 	BaseHandler | ||||
| 	clients     *types.LMap[int, *types.WsClient] | ||||
| 	userService *service.UserService | ||||
| } | ||||
|  | ||||
| func NewMarkMapHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *MarkMapHandler { | ||||
| 	return &MarkMapHandler{ | ||||
| 		BaseHandler: BaseHandler{App: app, DB: db}, | ||||
| 		clients:     types.NewLMap[int, *types.WsClient](), | ||||
| 		userService: userService, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Generate 生成思维导图 | ||||
| func (h *MarkMapHandler) Generate(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Prompt  string `json:"prompt"` | ||||
| 		ModelId int    `json:"model_id"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	var user model.User | ||||
| 	err := h.DB.Where("id", userId).First(&user, userId).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "error with query user info") | ||||
| 		return | ||||
| 	} | ||||
| 	var chatModel model.ChatModel | ||||
| 	err = h.DB.Where("id", data.ModelId).First(&chatModel).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "error with query chat model") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if user.Power < chatModel.Power { | ||||
| 		resp.ERROR(c, fmt.Sprintf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power)) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	messages := make([]interface{}, 0) | ||||
| 	messages = append(messages, types.Message{Role: "system", Content: ` | ||||
| 你是一位非常优秀的思维导图助手, 你能帮助用户整理思路,根据用户提供的主题或内容,快速生成结构清晰,有条理的思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子: | ||||
| # Geek-AI 助手 | ||||
|  | ||||
| ## 完整的开源系统 | ||||
| ### 前端开源 | ||||
| ### 后端开源 | ||||
|  | ||||
| ## 支持各种大模型 | ||||
| ### OpenAI  | ||||
| ### Azure  | ||||
| ### 文心一言 | ||||
| ### 通义千问 | ||||
|  | ||||
| ## 集成多种收费方式 | ||||
| ### 支付宝 | ||||
| ### 微信 | ||||
|  | ||||
| 请直接生成结果,不要任何解释性语句。 | ||||
| `}) | ||||
| 	messages = append(messages, types.Message{Role: "user", Content: fmt.Sprintf("请生成一份有关【%s】一份思维导图,要求结构清晰,有条理", data.Prompt)}) | ||||
| 	content, err := utils.SendOpenAIMessage(h.DB, messages, data.ModelId) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, fmt.Sprintf("请求 OpenAI API 失败: %s", err)) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 扣减算力 | ||||
| 	if chatModel.Power > 0 { | ||||
| 		err = h.userService.DecreasePower(int(userId), chatModel.Power, model.PowerLog{ | ||||
| 			Type:   types.PowerConsume, | ||||
| 			Model:  chatModel.Value, | ||||
| 			Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value), | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, "error with save power log, "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, content) | ||||
| } | ||||
							
								
								
									
										49
									
								
								api/handler/menu_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								api/handler/menu_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type MenuHandler struct { | ||||
| 	BaseHandler | ||||
| } | ||||
|  | ||||
| func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler { | ||||
| 	return &MenuHandler{BaseHandler: BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| // List 数据列表 | ||||
| func (h *MenuHandler) List(c *gin.Context) { | ||||
| 	index := h.GetBool(c, "index") | ||||
| 	var items []model.Menu | ||||
| 	var list = make([]vo.Menu, 0) | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	session = session.Where("enabled", true) | ||||
| 	if index { | ||||
| 		session = session.Where("id IN ?", h.App.SysConfig.IndexNavs) | ||||
| 	} | ||||
| 	res := session.Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var product vo.Menu | ||||
| 			err := utils.CopyObject(item, &product) | ||||
| 			if err == nil { | ||||
| 				list = append(list, product) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, list) | ||||
| } | ||||
							
								
								
									
										437
									
								
								api/handler/mj_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										437
									
								
								api/handler/mj_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,437 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/service" | ||||
| 	"geekai/service/mj" | ||||
| 	"geekai/service/oss" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type MidJourneyHandler struct { | ||||
| 	BaseHandler | ||||
| 	mjService   *mj.Service | ||||
| 	snowflake   *service.Snowflake | ||||
| 	uploader    *oss.UploaderManager | ||||
| 	userService *service.UserService | ||||
| } | ||||
|  | ||||
| func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService) *MidJourneyHandler { | ||||
| 	return &MidJourneyHandler{ | ||||
| 		snowflake:   snowflake, | ||||
| 		mjService:   service, | ||||
| 		uploader:    manager, | ||||
| 		userService: userService, | ||||
| 		BaseHandler: BaseHandler{ | ||||
| 			App: app, | ||||
| 			DB:  db, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) preCheck(c *gin.Context) bool { | ||||
| 	user, err := h.GetLoginUser(c) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if user.Power < h.App.SysConfig.MjPower { | ||||
| 		resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!") | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return true | ||||
|  | ||||
| } | ||||
|  | ||||
| // Image 创建一个绘画任务 | ||||
| func (h *MidJourneyHandler) Image(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		TaskType  string   `json:"task_type"` | ||||
| 		ClientId  string   `json:"client_id"` | ||||
| 		Prompt    string   `json:"prompt"` | ||||
| 		NegPrompt string   `json:"neg_prompt"` | ||||
| 		Rate      string   `json:"rate"` | ||||
| 		Model     string   `json:"model"`   // 模型 | ||||
| 		Chaos     int      `json:"chaos"`   // 创意度取值范围: 0-100 | ||||
| 		Raw       bool     `json:"raw"`     // 是否开启原始模型 | ||||
| 		Seed      int64    `json:"seed"`    // 随机数 | ||||
| 		Stylize   int      `json:"stylize"` // 风格化 | ||||
| 		ImgArr    []string `json:"img_arr"` | ||||
| 		Tile      bool     `json:"tile"`    // 重复平铺 | ||||
| 		Quality   float32  `json:"quality"` // 画质 | ||||
| 		Iw        float32  `json:"iw"` | ||||
| 		CRef      string   `json:"cref"` //生成角色一致的图像 | ||||
| 		SRef      string   `json:"sref"` //生成风格一致的图像 | ||||
| 		Cw        int      `json:"cw"`   // 参考程度 | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	if !h.preCheck(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var params = "" | ||||
| 	if data.Rate != "" && !strings.Contains(params, "--ar") { | ||||
| 		params += " --ar " + data.Rate | ||||
| 	} | ||||
| 	if data.Seed > 0 && !strings.Contains(params, "--seed") { | ||||
| 		params += fmt.Sprintf(" --seed %d", data.Seed) | ||||
| 	} | ||||
| 	if data.Stylize > 0 && !strings.Contains(params, "--s") && !strings.Contains(params, "--stylize") { | ||||
| 		params += fmt.Sprintf(" --s %d", data.Stylize) | ||||
| 	} | ||||
| 	if data.Chaos > 0 && !strings.Contains(params, "--c") && !strings.Contains(params, "--chaos") { | ||||
| 		params += fmt.Sprintf(" --c %d", data.Chaos) | ||||
| 	} | ||||
| 	if len(data.ImgArr) > 0 && data.Iw > 0 { | ||||
| 		params += fmt.Sprintf(" --iw %.2f", data.Iw) | ||||
| 	} | ||||
| 	if data.Raw { | ||||
| 		params += " --style raw" | ||||
| 	} | ||||
| 	if data.Quality > 0 { | ||||
| 		params += fmt.Sprintf(" --q %.2f", data.Quality) | ||||
| 	} | ||||
| 	if data.Tile { | ||||
| 		params += " --tile " | ||||
| 	} | ||||
| 	if data.CRef != "" { | ||||
| 		params += fmt.Sprintf(" --cref %s", data.CRef) | ||||
| 		if data.Cw > 0 { | ||||
| 			params += fmt.Sprintf(" --cw %d", data.Cw) | ||||
| 		} else { | ||||
| 			params += " --cw 100" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if data.SRef != "" { | ||||
| 		params += fmt.Sprintf(" --sref %s", data.SRef) | ||||
| 	} | ||||
| 	if data.Model != "" && !strings.Contains(params, "--v") && !strings.Contains(params, "--niji") { | ||||
| 		params += fmt.Sprintf(" %s", data.Model) | ||||
| 	} | ||||
|  | ||||
| 	// 处理融图和换脸的提示词 | ||||
| 	if data.TaskType == types.TaskSwapFace.String() || data.TaskType == types.TaskBlend.String() { | ||||
| 		params = fmt.Sprintf("%s:%s", data.TaskType, strings.Join(data.ImgArr, ",")) | ||||
| 	} | ||||
|  | ||||
| 	// 如果本地图片上传的是相对地址,处理成绝对地址 | ||||
| 	for k, v := range data.ImgArr { | ||||
| 		if !strings.HasPrefix(v, "http") { | ||||
| 			data.ImgArr[k] = fmt.Sprintf("http://localhost:5678/%s", strings.TrimLeft(v, "/")) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	// generate task id | ||||
| 	taskId, err := h.snowflake.Next(true) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "error with generate task id: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	task := types.MjTask{ | ||||
| 		ClientId:         data.ClientId, | ||||
| 		TaskId:           taskId, | ||||
| 		Type:             types.TaskType(data.TaskType), | ||||
| 		Prompt:           data.Prompt, | ||||
| 		NegPrompt:        data.NegPrompt, | ||||
| 		Params:           params, | ||||
| 		UserId:           userId, | ||||
| 		ImgArr:           data.ImgArr, | ||||
| 		Mode:             h.App.SysConfig.MjMode, | ||||
| 		TranslateModelId: h.App.SysConfig.TranslateModelId, | ||||
| 	} | ||||
| 	job := model.MidJourneyJob{ | ||||
| 		Type:      data.TaskType, | ||||
| 		UserId:    userId, | ||||
| 		TaskId:    taskId, | ||||
| 		TaskInfo:  utils.JsonEncode(task), | ||||
| 		Progress:  0, | ||||
| 		Prompt:    fmt.Sprintf("%s %s", data.Prompt, params), | ||||
| 		Power:     h.App.SysConfig.MjPower, | ||||
| 		CreatedAt: time.Now(), | ||||
| 	} | ||||
| 	opt := "绘图" | ||||
| 	if data.TaskType == types.TaskBlend.String() { | ||||
| 		job.Prompt = "融图:" + strings.Join(data.ImgArr, ",") | ||||
| 		opt = "融图" | ||||
| 	} else if data.TaskType == types.TaskSwapFace.String() { | ||||
| 		job.Prompt = "换脸:" + strings.Join(data.ImgArr, ",") | ||||
| 		opt = "换脸" | ||||
| 	} | ||||
|  | ||||
| 	if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { | ||||
| 		resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	task.Id = job.Id | ||||
| 	h.mjService.PushTask(task) | ||||
|  | ||||
| 	// update user's power | ||||
| 	err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ | ||||
| 		Type:   types.PowerConsume, | ||||
| 		Model:  "mid-journey", | ||||
| 		Remark: fmt.Sprintf("%s操作,任务ID:%s", opt, job.TaskId), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| type reqVo struct { | ||||
| 	Index       int    `json:"index"` | ||||
| 	ClientId    string `json:"client_id"` | ||||
| 	ChannelId   string `json:"channel_id"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| } | ||||
|  | ||||
| // Upscale send upscale command to MidJourney Bot | ||||
| func (h *MidJourneyHandler) Upscale(c *gin.Context) { | ||||
| 	var data reqVo | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !h.preCheck(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	taskId, _ := h.snowflake.Next(true) | ||||
| 	task := types.MjTask{ | ||||
| 		ClientId:    data.ClientId, | ||||
| 		Type:        types.TaskUpscale, | ||||
| 		UserId:      userId, | ||||
| 		ChannelId:   data.ChannelId, | ||||
| 		Index:       data.Index, | ||||
| 		MessageId:   data.MessageId, | ||||
| 		MessageHash: data.MessageHash, | ||||
| 		Mode:        h.App.SysConfig.MjMode, | ||||
| 	} | ||||
| 	job := model.MidJourneyJob{ | ||||
| 		Type:      types.TaskUpscale.String(), | ||||
| 		UserId:    userId, | ||||
| 		TaskId:    taskId, | ||||
| 		TaskInfo:  utils.JsonEncode(task), | ||||
| 		Progress:  0, | ||||
| 		Power:     h.App.SysConfig.MjActionPower, | ||||
| 		CreatedAt: time.Now(), | ||||
| 	} | ||||
| 	if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { | ||||
| 		resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	task.Id = job.Id | ||||
| 	h.mjService.PushTask(task) | ||||
|  | ||||
| 	// update user's power | ||||
| 	err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ | ||||
| 		Type:   types.PowerConsume, | ||||
| 		Model:  "mid-journey", | ||||
| 		Remark: fmt.Sprintf("Upscale 操作,任务ID:%s", job.TaskId), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Variation send variation command to MidJourney Bot | ||||
| func (h *MidJourneyHandler) Variation(c *gin.Context) { | ||||
| 	var data reqVo | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !h.preCheck(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	taskId, _ := h.snowflake.Next(true) | ||||
| 	task := types.MjTask{ | ||||
| 		Type:        types.TaskVariation, | ||||
| 		ClientId:    data.ClientId, | ||||
| 		UserId:      userId, | ||||
| 		Index:       data.Index, | ||||
| 		ChannelId:   data.ChannelId, | ||||
| 		MessageId:   data.MessageId, | ||||
| 		MessageHash: data.MessageHash, | ||||
| 		Mode:        h.App.SysConfig.MjMode, | ||||
| 	} | ||||
| 	job := model.MidJourneyJob{ | ||||
| 		Type:      types.TaskVariation.String(), | ||||
| 		ChannelId: data.ChannelId, | ||||
| 		UserId:    userId, | ||||
| 		TaskId:    taskId, | ||||
| 		TaskInfo:  utils.JsonEncode(task), | ||||
| 		Progress:  0, | ||||
| 		Power:     h.App.SysConfig.MjActionPower, | ||||
| 		CreatedAt: time.Now(), | ||||
| 	} | ||||
| 	if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { | ||||
| 		resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	task.Id = job.Id | ||||
| 	h.mjService.PushTask(task) | ||||
|  | ||||
| 	err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ | ||||
| 		Type:   types.PowerConsume, | ||||
| 		Model:  "mid-journey", | ||||
| 		Remark: fmt.Sprintf("Variation 操作,任务ID:%s", job.TaskId), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // ImgWall 照片墙 | ||||
| func (h *MidJourneyHandler) ImgWall(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 0) | ||||
| 	pageSize := h.GetInt(c, "page_size", 0) | ||||
| 	err, jobs := h.getData(true, 0, page, pageSize, true) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, jobs) | ||||
| } | ||||
|  | ||||
| // JobList 获取 MJ 任务列表 | ||||
| func (h *MidJourneyHandler) JobList(c *gin.Context) { | ||||
| 	finish := h.GetBool(c, "finish") | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	page := h.GetInt(c, "page", 0) | ||||
| 	pageSize := h.GetInt(c, "page_size", 0) | ||||
| 	publish := h.GetBool(c, "publish") | ||||
|  | ||||
| 	err, jobs := h.getData(finish, userId, page, pageSize, publish) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, jobs) | ||||
| } | ||||
|  | ||||
| // JobList 获取 MJ 任务列表 | ||||
| func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) { | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	if finish { | ||||
| 		session = session.Where("progress >= ?", 100).Order("id DESC") | ||||
| 	} else { | ||||
| 		session = session.Where("progress < ?", 100).Order("id ASC") | ||||
| 	} | ||||
| 	if userId > 0 { | ||||
| 		session = session.Where("user_id = ?", userId) | ||||
| 	} | ||||
| 	if publish { | ||||
| 		session = session.Where("publish = ?", publish) | ||||
| 	} | ||||
| 	if page > 0 && pageSize > 0 { | ||||
| 		offset := (page - 1) * pageSize | ||||
| 		session = session.Offset(offset).Limit(pageSize) | ||||
| 	} | ||||
|  | ||||
| 	// 统计总数 | ||||
| 	var total int64 | ||||
| 	session.Model(&model.MidJourneyJob{}).Count(&total) | ||||
|  | ||||
| 	var items []model.MidJourneyJob | ||||
| 	res := session.Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		return res.Error, vo.Page{} | ||||
| 	} | ||||
|  | ||||
| 	var jobs = make([]vo.MidJourneyJob, 0) | ||||
| 	for _, item := range items { | ||||
| 		var job vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(item, &job) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		jobs = append(jobs, job) | ||||
| 	} | ||||
| 	return nil, vo.NewPage(total, page, pageSize, jobs) | ||||
| } | ||||
|  | ||||
| // Remove remove task image | ||||
| func (h *MidJourneyHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	userId := h.GetInt(c, "user_id", 0) | ||||
| 	var job model.MidJourneyJob | ||||
| 	if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil { | ||||
| 		resp.ERROR(c, "记录不存在") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// remove job | ||||
| 	err := h.DB.Delete(&job).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// remove image | ||||
| 	err = h.uploader.GetUploadHandler().Delete(job.ImgURL) | ||||
| 	if err != nil { | ||||
| 		logger.Error("remove image failed: ", err) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Publish 发布图片到画廊显示 | ||||
| func (h *MidJourneyHandler) Publish(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	userId := h.GetInt(c, "user_id", 0) | ||||
| 	action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享 | ||||
| 	err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										161
									
								
								api/handler/net_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								api/handler/net_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/service/oss" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type NetHandler struct { | ||||
| 	BaseHandler | ||||
| 	uploaderManager *oss.UploaderManager | ||||
| } | ||||
|  | ||||
| func NewNetHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *NetHandler { | ||||
| 	return &NetHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager} | ||||
| } | ||||
|  | ||||
| func (h *NetHandler) Upload(c *gin.Context) { | ||||
| 	file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file") | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	logger.Info("upload file: ", file.Name) | ||||
| 	// cut the file name if it's too long | ||||
| 	if len(file.Name) > 100 { | ||||
| 		file.Name = file.Name[:90] + file.Ext | ||||
| 	} | ||||
|  | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	res := h.DB.Create(&model.File{ | ||||
| 		UserId:    int(userId), | ||||
| 		Name:      file.Name, | ||||
| 		ObjKey:    file.ObjKey, | ||||
| 		URL:       file.URL, | ||||
| 		Ext:       file.Ext, | ||||
| 		Size:      file.Size, | ||||
| 		CreatedAt: time.Time{}, | ||||
| 	}) | ||||
| 	if res.Error != nil || res.RowsAffected == 0 { | ||||
| 		resp.ERROR(c, "error with update database: "+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, file) | ||||
| } | ||||
|  | ||||
| func (h *NetHandler) List(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Urls     []string `json:"urls,omitempty"` | ||||
| 		Page     int      `json:"page"` | ||||
| 		PageSize int      `json:"page_size"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	var items []model.File | ||||
| 	var files = make([]vo.File, 0) | ||||
| 	session := h.DB.Session(&gorm.Session{}) | ||||
| 	session = session.Where("user_id = ?", userId) | ||||
| 	if len(data.Urls) > 0 { | ||||
| 		session = session.Where("url IN ?", data.Urls) | ||||
| 	} | ||||
| 	// 统计总数 | ||||
| 	var total int64 | ||||
| 	session.Model(&model.File{}).Count(&total) | ||||
|  | ||||
| 	if data.Page > 0 && data.PageSize > 0 { | ||||
| 		offset := (data.Page - 1) * data.PageSize | ||||
| 		session = session.Offset(offset).Limit(data.PageSize) | ||||
| 	} | ||||
| 	err := session.Order("id desc").Find(&items).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range items { | ||||
| 		var file vo.File | ||||
| 		err := utils.CopyObject(v, &file) | ||||
| 		if err != nil { | ||||
| 			logger.Error(err) | ||||
| 			continue | ||||
| 		} | ||||
| 		file.CreatedAt = v.CreatedAt.Unix() | ||||
| 		files = append(files, file) | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, files)) | ||||
| } | ||||
|  | ||||
| // Remove remove files | ||||
| func (h *NetHandler) Remove(c *gin.Context) { | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
| 	var file model.File | ||||
| 	tx := h.DB.Where("user_id = ? AND id = ?", userId, id).First(&file) | ||||
| 	if tx.Error != nil || file.Id == 0 { | ||||
| 		resp.ERROR(c, "file not existed") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// remove database | ||||
| 	tx = h.DB.Model(&model.File{}).Delete("id = ?", id) | ||||
| 	if tx.Error != nil || tx.RowsAffected == 0 { | ||||
| 		resp.ERROR(c, "failed to update database") | ||||
| 		return | ||||
| 	} | ||||
| 	// remove files | ||||
| 	objectKey := file.ObjKey | ||||
| 	if objectKey == "" { | ||||
| 		objectKey = file.URL | ||||
| 	} | ||||
| 	_ = h.uploaderManager.GetUploadHandler().Delete(objectKey) | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *NetHandler) Download(c *gin.Context) { | ||||
| 	fileUrl := c.Query("url") | ||||
| 	// 使用http工具下载文件 | ||||
| 	if fileUrl == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	// 使用http.Get下载文件 | ||||
| 	r, err := http.Get(fileUrl) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	defer r.Body.Close() | ||||
|  | ||||
| 	if r.StatusCode != http.StatusOK { | ||||
| 		resp.ERROR(c, "error status:"+r.Status) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.Status(http.StatusOK) | ||||
| 	// 将下载的文件内容写入响应 | ||||
| 	_, _ = io.Copy(c.Writer, r.Body) | ||||
| } | ||||
							
								
								
									
										226
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	req2 "github.com/imroc/req/v3" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type Usage struct { | ||||
| 	Prompt           string `json:"prompt,omitempty"` | ||||
| 	Content          string `json:"content,omitempty"` | ||||
| 	PromptTokens     int    `json:"prompt_tokens"` | ||||
| 	CompletionTokens int    `json:"completion_tokens"` | ||||
| 	TotalTokens      int    `json:"total_tokens"` | ||||
| } | ||||
|  | ||||
| type OpenAIResVo struct { | ||||
| 	Id                string `json:"id"` | ||||
| 	Object            string `json:"object"` | ||||
| 	Created           int    `json:"created"` | ||||
| 	Model             string `json:"model"` | ||||
| 	SystemFingerprint string `json:"system_fingerprint"` | ||||
| 	Choices           []struct { | ||||
| 		Index   int `json:"index"` | ||||
| 		Message struct { | ||||
| 			Role    string `json:"role"` | ||||
| 			Content string `json:"content"` | ||||
| 		} `json:"message"` | ||||
| 		Logprobs     interface{} `json:"logprobs"` | ||||
| 		FinishReason string      `json:"finish_reason"` | ||||
| 	} `json:"choices"` | ||||
| 	Usage Usage `json:"usage"` | ||||
| } | ||||
|  | ||||
| // OPenAI 消息发送实现 | ||||
| func (h *ChatHandler) sendOpenAiMessage( | ||||
| 	req types.ApiRequest, | ||||
| 	userVo vo.User, | ||||
| 	ctx context.Context, | ||||
| 	session *types.ChatSession, | ||||
| 	role model.ChatRole, | ||||
| 	prompt string, | ||||
| 	ws *types.WsClient) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 	start := time.Now() | ||||
| 	var apiKey = model.ApiKey{} | ||||
| 	response, err := h.doRequest(ctx, req, session, &apiKey) | ||||
| 	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start)) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			return fmt.Errorf("用户取消了请求:%s", prompt) | ||||
| 		} else if strings.Contains(err.Error(), "no available key") { | ||||
| 			return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") | ||||
| 		} | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != 200 { | ||||
| 		body, _ := io.ReadAll(response.Body) | ||||
| 		return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body)) | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() // 记录回复时间 | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{Role: "assistant"} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var function model.Function | ||||
| 		var toolCall = false | ||||
| 		var arguments = make([]string, 0) | ||||
| 		scanner := bufio.NewScanner(response.Body) | ||||
| 		for scanner.Scan() { | ||||
| 			line := scanner.Text() | ||||
| 			if !strings.Contains(line, "data:") || len(line) < 30 { | ||||
| 				continue | ||||
| 			} | ||||
| 			var responseBody = types.ApiResponse{} | ||||
| 			err = json.Unmarshal([]byte(line[6:]), &responseBody) | ||||
| 			if err != nil { // 数据解析出错 | ||||
| 				return errors.New(line) | ||||
| 			} | ||||
| 			if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 | ||||
| 				continue | ||||
| 			} | ||||
| 			if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 { | ||||
| 				utils.SendChunkMsg(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。") | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			var tool types.ToolCall | ||||
| 			if len(responseBody.Choices[0].Delta.ToolCalls) > 0 { | ||||
| 				tool = responseBody.Choices[0].Delta.ToolCalls[0] | ||||
| 				if toolCall && tool.Function.Name == "" { | ||||
| 					arguments = append(arguments, tool.Function.Arguments) | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// 兼容 Function Call | ||||
| 			fun := responseBody.Choices[0].Delta.FunctionCall | ||||
| 			if fun.Name != "" { | ||||
| 				tool = *new(types.ToolCall) | ||||
| 				tool.Function.Name = fun.Name | ||||
| 			} else if toolCall { | ||||
| 				arguments = append(arguments, fun.Arguments) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if !utils.IsEmptyValue(tool) { | ||||
| 				res := h.DB.Where("name = ?", tool.Function.Name).First(&function) | ||||
| 				if res.Error == nil { | ||||
| 					toolCall = true | ||||
| 					callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label) | ||||
| 					utils.SendChunkMsg(ws, callMsg) | ||||
| 					contents = append(contents, callMsg) | ||||
| 				} | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if responseBody.Choices[0].FinishReason == "tool_calls" || | ||||
| 				responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// output stopped | ||||
| 			if responseBody.Choices[0].FinishReason != "" { | ||||
| 				break // 输出完成或者输出中断了 | ||||
| 			} else { | ||||
| 				content := responseBody.Choices[0].Delta.Content | ||||
| 				contents = append(contents, utils.InterfaceToString(content)) | ||||
| 				utils.SendChunkMsg(ws, responseBody.Choices[0].Delta.Content) | ||||
| 			} | ||||
| 		} // end for | ||||
|  | ||||
| 		if err := scanner.Err(); err != nil { | ||||
| 			if strings.Contains(err.Error(), "context canceled") { | ||||
| 				logger.Info("用户取消了请求:", prompt) | ||||
| 			} else { | ||||
| 				logger.Error("信息读取出错:", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if toolCall { // 调用函数完成任务 | ||||
| 			params := make(map[string]interface{}) | ||||
| 			_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) | ||||
| 			logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params) | ||||
| 			params["user_id"] = userVo.Id | ||||
| 			var apiRes types.BizVo | ||||
| 			r, err := req2.C().R().SetHeader("Body-Type", "application/json"). | ||||
| 				SetHeader("Authorization", function.Token). | ||||
| 				SetBody(params). | ||||
| 				SetSuccessResult(&apiRes).Post(function.Action) | ||||
| 			errMsg := "" | ||||
| 			if err != nil { | ||||
| 				errMsg = err.Error() | ||||
| 			} else if r.IsErrorState() { | ||||
| 				errMsg = r.Status | ||||
| 			} | ||||
| 			if errMsg != "" || apiRes.Code != types.Success { | ||||
| 				errMsg = "调用函数工具出错:" + apiRes.Message + errMsg | ||||
| 				contents = append(contents, errMsg) | ||||
| 			} else { | ||||
| 				errMsg = utils.InterfaceToString(apiRes.Data) | ||||
| 				contents = append(contents, errMsg) | ||||
| 			} | ||||
| 			utils.SendChunkMsg(ws, errMsg) | ||||
| 		} | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			usage := Usage{ | ||||
| 				Prompt:           prompt, | ||||
| 				Content:          strings.Join(contents, ""), | ||||
| 				PromptTokens:     0, | ||||
| 				CompletionTokens: 0, | ||||
| 				TotalTokens:      0, | ||||
| 			} | ||||
| 			message.Content = usage.Content | ||||
| 			h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) | ||||
| 		} | ||||
| 	} else { // 非流式输出 | ||||
| 		var respVo OpenAIResVo | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("读取响应失败:%v", body) | ||||
| 		} | ||||
| 		err = json.Unmarshal(body, &respVo) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("解析响应失败:%v", body) | ||||
| 		} | ||||
| 		content := respVo.Choices[0].Message.Content | ||||
| 		if strings.HasPrefix(req.Model, "o1-") { | ||||
| 			content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content) | ||||
| 		} | ||||
| 		utils.SendChunkMsg(ws, content) | ||||
| 		respVo.Usage.Prompt = prompt | ||||
| 		respVo.Usage.Content = content | ||||
| 		h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now()) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										98
									
								
								api/handler/order_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								api/handler/order_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/store/vo" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type OrderHandler struct { | ||||
| 	BaseHandler | ||||
| } | ||||
|  | ||||
| func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler { | ||||
| 	return &OrderHandler{BaseHandler: BaseHandler{App: app, DB: db}} | ||||
| } | ||||
|  | ||||
| // List 订单列表 | ||||
| func (h *OrderHandler) List(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 1) | ||||
| 	pageSize := h.GetInt(c, "page_size", 20) | ||||
| 	userId := h.GetLoginUserId(c) | ||||
| 	session := h.DB.Session(&gorm.Session{}).Where("user_id = ? AND status = ?", userId, types.OrderPaidSuccess) | ||||
| 	var total int64 | ||||
| 	session.Model(&model.Order{}).Count(&total) | ||||
| 	var items []model.Order | ||||
| 	var list = make([]vo.Order, 0) | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var order vo.Order | ||||
| 			err := utils.CopyObject(item, &order) | ||||
| 			if err == nil { | ||||
| 				order.Id = item.Id | ||||
| 				order.CreatedAt = item.CreatedAt.Unix() | ||||
| 				order.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				payMethod, ok := types.PayMethods[item.PayWay] | ||||
| 				if !ok { | ||||
| 					payMethod = item.PayWay | ||||
| 				} | ||||
| 				payName, ok := types.PayNames[item.PayType] | ||||
| 				if !ok { | ||||
| 					payName = item.PayWay | ||||
| 				} | ||||
| 				order.PayMethod = payMethod | ||||
| 				order.PayName = payName | ||||
| 				list = append(list, order) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list)) | ||||
| } | ||||
|  | ||||
| // Query 查询订单状态 | ||||
| func (h *OrderHandler) Query(c *gin.Context) { | ||||
| 	orderNo := h.GetTrim(c, "order_no") | ||||
| 	var order model.Order | ||||
| 	res := h.DB.Where("order_no = ?", orderNo).First(&order) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Order not found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if order.Status == types.OrderPaidSuccess { | ||||
| 		resp.SUCCESS(c, gin.H{"status": order.Status}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	counter := 0 | ||||
| 	for { | ||||
| 		time.Sleep(time.Second) | ||||
| 		var item model.Order | ||||
| 		h.DB.Where("order_no = ?", orderNo).First(&item) | ||||
| 		if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status { | ||||
| 			order.Status = item.Status | ||||
| 			break | ||||
| 		} | ||||
| 		counter++ | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, gin.H{"status": order.Status}) | ||||
| } | ||||
							
								
								
									
										453
									
								
								api/handler/payment_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										453
									
								
								api/handler/payment_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,453 @@ | ||||
| package handler | ||||
|  | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
| // * Copyright 2023 The Geek-AI Authors. All rights reserved. | ||||
| // * Use of this source code is governed by a Apache-2.0 license | ||||
| // * that can be found in the LICENSE file. | ||||
| // * @Author yangjian102621@163.com | ||||
| // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| import ( | ||||
| 	"embed" | ||||
| 	"fmt" | ||||
| 	"geekai/core" | ||||
| 	"geekai/core/types" | ||||
| 	"geekai/service" | ||||
| 	"geekai/service/payment" | ||||
| 	"geekai/store/model" | ||||
| 	"geekai/utils" | ||||
| 	"geekai/utils/resp" | ||||
| 	"net/http" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type PayWay struct { | ||||
| 	Name  string `json:"name"` | ||||
| 	Value string `json:"value"` | ||||
| } | ||||
|  | ||||
| // PaymentHandler 支付服务回调 handler | ||||
| type PaymentHandler struct { | ||||
| 	BaseHandler | ||||
| 	alipayService    *payment.AlipayService | ||||
| 	huPiPayService   *payment.HuPiPayService | ||||
| 	geekPayService   *payment.GeekPayService | ||||
| 	wechatPayService *payment.WechatPayService | ||||
| 	snowflake        *service.Snowflake | ||||
| 	userService      *service.UserService | ||||
| 	fs               embed.FS | ||||
| 	lock             sync.Mutex | ||||
| 	signKey          string // 用来签名的随机秘钥 | ||||
| } | ||||
|  | ||||
| func NewPaymentHandler( | ||||
| 	server *core.AppServer, | ||||
| 	alipayService *payment.AlipayService, | ||||
| 	huPiPayService *payment.HuPiPayService, | ||||
| 	geekPayService *payment.GeekPayService, | ||||
| 	wechatPayService *payment.WechatPayService, | ||||
| 	db *gorm.DB, | ||||
| 	userService *service.UserService, | ||||
| 	snowflake *service.Snowflake, | ||||
| 	fs embed.FS) *PaymentHandler { | ||||
| 	return &PaymentHandler{ | ||||
| 		alipayService:    alipayService, | ||||
| 		huPiPayService:   huPiPayService, | ||||
| 		geekPayService:   geekPayService, | ||||
| 		wechatPayService: wechatPayService, | ||||
| 		snowflake:        snowflake, | ||||
| 		userService:      userService, | ||||
| 		fs:               fs, | ||||
| 		lock:             sync.Mutex{}, | ||||
| 		BaseHandler: BaseHandler{ | ||||
| 			App: server, | ||||
| 			DB:  db, | ||||
| 		}, | ||||
| 		signKey: utils.RandString(32), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (h *PaymentHandler) Pay(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		PayWay    string `json:"pay_way"` | ||||
| 		PayType   string `json:"pay_type"` | ||||
| 		ProductId int    `json:"product_id"` | ||||
| 		UserId    int    `json:"user_id"` | ||||
| 		Device    string `json:"device"` | ||||
| 		Host      string `json:"host"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var product model.Product | ||||
| 	err := h.DB.Where("id", data.ProductId).First(&product).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "Product not found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	orderNo, err := h.snowflake.Next(false) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "error with generate trade no: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	var user model.User | ||||
| 	err = h.DB.Where("id", data.UserId).First(&user).Error | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	amount := product.Discount | ||||
| 	var payURL, returnURL, notifyURL string | ||||
| 	switch data.PayWay { | ||||
| 	case "alipay": | ||||
| 		if h.App.Config.AlipayConfig.NotifyURL != "" { // 用于本地调试支付 | ||||
| 			notifyURL = h.App.Config.AlipayConfig.NotifyURL | ||||
| 		} else { | ||||
| 			notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Host) | ||||
| 		} | ||||
| 		if h.App.Config.AlipayConfig.ReturnURL != "" { // 用于本地调试支付 | ||||
| 			returnURL = h.App.Config.AlipayConfig.ReturnURL | ||||
| 		} else { | ||||
| 			returnURL = fmt.Sprintf("%s/payReturn", data.Host) | ||||
| 		} | ||||
| 		money := fmt.Sprintf("%.2f", amount) | ||||
| 		if data.Device == "wechat" { | ||||
| 			payURL, err = h.alipayService.PayMobile(payment.AlipayParams{ | ||||
| 				OutTradeNo: orderNo, | ||||
| 				Subject:    product.Name, | ||||
| 				TotalFee:   money, | ||||
| 				ReturnURL:  returnURL, | ||||
| 				NotifyURL:  notifyURL, | ||||
| 			}) | ||||
| 		} else { | ||||
| 			payURL, err = h.alipayService.PayPC(payment.AlipayParams{ | ||||
| 				OutTradeNo: orderNo, | ||||
| 				Subject:    product.Name, | ||||
| 				TotalFee:   money, | ||||
| 				ReturnURL:  returnURL, | ||||
| 				NotifyURL:  notifyURL, | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, "error with generate pay url: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		break | ||||
| 	case "wechat": | ||||
| 		if h.App.Config.WechatPayConfig.NotifyURL != "" { | ||||
| 			notifyURL = h.App.Config.WechatPayConfig.NotifyURL | ||||
| 		} else { | ||||
| 			notifyURL = fmt.Sprintf("%s/api/payment/notify/wechat", data.Host) | ||||
| 		} | ||||
| 		if data.Device == "wechat" { | ||||
| 			payURL, err = h.wechatPayService.PayUrlH5(payment.WechatPayParams{ | ||||
| 				OutTradeNo: orderNo, | ||||
| 				TotalFee:   int(amount * 100), | ||||
| 				Subject:    product.Name, | ||||
| 				NotifyURL:  notifyURL, | ||||
| 				ClientIP:   c.ClientIP(), | ||||
| 			}) | ||||
| 		} else { | ||||
| 			payURL, err = h.wechatPayService.PayUrlNative(payment.WechatPayParams{ | ||||
| 				OutTradeNo: orderNo, | ||||
| 				TotalFee:   int(amount * 100), | ||||
| 				Subject:    product.Name, | ||||
| 				NotifyURL:  notifyURL, | ||||
| 			}) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		break | ||||
| 	case "hupi": | ||||
| 		if h.App.Config.HuPiPayConfig.NotifyURL != "" { | ||||
| 			notifyURL = h.App.Config.HuPiPayConfig.NotifyURL | ||||
| 		} else { | ||||
| 			notifyURL = fmt.Sprintf("%s/api/payment/notify/hupi", data.Host) | ||||
| 		} | ||||
| 		if h.App.Config.HuPiPayConfig.ReturnURL != "" { | ||||
| 			returnURL = h.App.Config.HuPiPayConfig.ReturnURL | ||||
| 		} else { | ||||
| 			returnURL = fmt.Sprintf("%s/payReturn", data.Host) | ||||
| 		} | ||||
| 		r, err := h.huPiPayService.Pay(payment.HuPiPayParams{ | ||||
| 			Version:      "1.1", | ||||
| 			TradeOrderId: orderNo, | ||||
| 			TotalFee:     fmt.Sprintf("%f", amount), | ||||
| 			Title:        product.Name, | ||||
| 			NotifyURL:    notifyURL, | ||||
| 			ReturnURL:    returnURL, | ||||
| 			WapName:      "GeekAI助手", | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		payURL = r.URL | ||||
| 		break | ||||
| 	case "geek": | ||||
| 		if h.App.Config.GeekPayConfig.NotifyURL != "" { | ||||
| 			notifyURL = h.App.Config.GeekPayConfig.NotifyURL | ||||
| 		} else { | ||||
| 			notifyURL = fmt.Sprintf("%s/api/payment/notify/geek", data.Host) | ||||
| 		} | ||||
| 		if h.App.Config.GeekPayConfig.ReturnURL != "" { | ||||
| 			data.Host = utils.GetBaseURL(h.App.Config.GeekPayConfig.ReturnURL) | ||||
| 		} | ||||
| 		if data.Device == "wechat" { // 微信客户端打开,调回手机端用户中心页面 | ||||
| 			returnURL = fmt.Sprintf("%s/mobile/profile", data.Host) | ||||
| 		} else { | ||||
| 			returnURL = fmt.Sprintf("%s/payReturn", data.Host) | ||||
| 		} | ||||
| 		params := payment.GeekPayParams{ | ||||
| 			OutTradeNo: orderNo, | ||||
| 			Method:     "web", | ||||
| 			Name:       product.Name, | ||||
| 			Money:      fmt.Sprintf("%f", amount), | ||||
| 			ClientIP:   c.ClientIP(), | ||||
| 			Device:     data.Device, | ||||
| 			Type:       data.PayType, | ||||
| 			ReturnURL:  returnURL, | ||||
| 			NotifyURL:  notifyURL, | ||||
| 		} | ||||
|  | ||||
| 		res, err := h.geekPayService.Pay(params) | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		payURL = res.PayURL | ||||
| 	default: | ||||
| 		resp.ERROR(c, "不支持的支付渠道") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 创建订单 | ||||
| 	remark := types.OrderRemark{ | ||||
| 		Days:     product.Days, | ||||
| 		Power:    product.Power, | ||||
| 		Name:     product.Name, | ||||
| 		Price:    product.Price, | ||||
| 		Discount: product.Discount, | ||||
| 	} | ||||
| 	order := model.Order{ | ||||
| 		UserId:    user.Id, | ||||
| 		Username:  user.Username, | ||||
| 		ProductId: product.Id, | ||||
| 		OrderNo:   orderNo, | ||||
| 		Subject:   product.Name, | ||||
| 		Amount:    amount, | ||||
| 		Status:    types.OrderNotPaid, | ||||
| 		PayWay:    data.PayWay, | ||||
| 		PayType:   data.PayType, | ||||
| 		Remark:    utils.JsonEncode(remark), | ||||
| 	} | ||||
| 	err = h.DB.Create(&order).Error | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "error with create order: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c, payURL) | ||||
| } | ||||
|  | ||||
| // 异步通知回调公共逻辑 | ||||
| func (h *PaymentHandler) notify(orderNo string, tradeNo string) error { | ||||
| 	var order model.Order | ||||
| 	err := h.DB.Where("order_no = ?", orderNo).First(&order).Error | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error with fetch order: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	h.lock.Lock() | ||||
| 	defer h.lock.Unlock() | ||||
|  | ||||
| 	// 已支付订单,直接返回 | ||||
| 	if order.Status == types.OrderPaidSuccess { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	var user model.User | ||||
| 	err = h.DB.First(&user, order.UserId).Error | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error with fetch user info: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	var remark types.OrderRemark | ||||
| 	err = utils.JsonDecode(order.Remark, &remark) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error with decode order remark: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// 增加用户算力 | ||||
| 	err = h.userService.IncreasePower(int(order.UserId), remark.Power, model.PowerLog{ | ||||
| 		Type:   types.PowerRecharge, | ||||
| 		Model:  order.PayWay, | ||||
| 		Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// 更新订单状态 | ||||
| 	order.PayTime = time.Now().Unix() | ||||
| 	order.Status = types.OrderPaidSuccess | ||||
| 	order.TradeNo = tradeNo | ||||
| 	err = h.DB.Updates(&order).Error | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error with update order info: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// 更新产品销量 | ||||
| 	err = h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId). | ||||
| 		UpdateColumn("sales", gorm.Expr("sales + ?", 1)).Error | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error with update product sales: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetPayWays 获取支付方式 | ||||
| func (h *PaymentHandler) GetPayWays(c *gin.Context) { | ||||
| 	payWays := make([]gin.H, 0) | ||||
| 	if h.App.Config.AlipayConfig.Enabled { | ||||
| 		payWays = append(payWays, gin.H{"pay_way": "alipay", "pay_type": "alipay"}) | ||||
| 	} | ||||
| 	if h.App.Config.HuPiPayConfig.Enabled { | ||||
| 		payWays = append(payWays, gin.H{"pay_way": "hupi", "pay_type": "wxpay"}) | ||||
| 	} | ||||
| 	if h.App.Config.GeekPayConfig.Enabled { | ||||
| 		for _, v := range h.App.Config.GeekPayConfig.Methods { | ||||
| 			payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": v}) | ||||
| 		} | ||||
| 	} | ||||
| 	if h.App.Config.WechatPayConfig.Enabled { | ||||
| 		payWays = append(payWays, gin.H{"pay_way": "wechat", "pay_type": "wxpay"}) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, payWays) | ||||
| } | ||||
|  | ||||
| // HuPiPayNotify 虎皮椒支付异步回调 | ||||
| func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) { | ||||
| 	err := c.Request.ParseForm() | ||||
| 	if err != nil { | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	orderNo := c.Request.Form.Get("trade_order_id") | ||||
| 	tradeNo := c.Request.Form.Get("open_order_id") | ||||
| 	logger.Infof("收到虎皮椒订单支付回调,%+v", c.Request.Form) | ||||
|  | ||||
| 	if err = h.huPiPayService.Check(orderNo); err != nil { | ||||
| 		logger.Error("订单校验失败:", err) | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = h.notify(orderNo, tradeNo) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.String(http.StatusOK, "success") | ||||
| } | ||||
|  | ||||
| // AlipayNotify 支付宝支付回调 | ||||
| func (h *PaymentHandler) AlipayNotify(c *gin.Context) { | ||||
| 	err := c.Request.ParseForm() | ||||
| 	if err != nil { | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	result := h.alipayService.TradeVerify(c.Request) | ||||
| 	logger.Infof("收到支付宝商号订单支付回调:%+v", result) | ||||
| 	if !result.Success() { | ||||
| 		logger.Error("订单校验失败:", result.Message) | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tradeNo := c.Request.Form.Get("trade_no") | ||||
| 	err = h.notify(result.OutTradeNo, tradeNo) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.String(http.StatusOK, "success") | ||||
| } | ||||
|  | ||||
| // GeekPayNotify 支付异步回调 | ||||
| func (h *PaymentHandler) GeekPayNotify(c *gin.Context) { | ||||
| 	var params = make(map[string]string) | ||||
| 	for k := range c.Request.URL.Query() { | ||||
| 		params[k] = c.Query(k) | ||||
| 	} | ||||
|  | ||||
| 	logger.Infof("收到GeekPay订单支付回调:%+v", params) | ||||
| 	// 检查支付状态 | ||||
| 	if params["trade_status"] != "TRADE_SUCCESS" { | ||||
| 		c.String(http.StatusOK, "success") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	sign := h.geekPayService.Sign(params) | ||||
| 	if sign != c.Query("sign") { | ||||
| 		logger.Errorf("签名验证失败, %s, %s", sign, c.Query("sign")) | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.notify(params["out_trade_no"], params["trade_no"]) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.String(http.StatusOK, "success") | ||||
| } | ||||
|  | ||||
| // WechatPayNotify 微信商户支付异步回调 | ||||
| func (h *PaymentHandler) WechatPayNotify(c *gin.Context) { | ||||
| 	err := c.Request.ParseForm() | ||||
| 	if err != nil { | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	result := h.wechatPayService.TradeVerify(c.Request) | ||||
| 	logger.Infof("收到微信商号订单支付回调:%+v", result) | ||||
| 	if !result.Success() { | ||||
| 		logger.Error("订单校验失败:", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{ | ||||
| 			"code":    "FAIL", | ||||
| 			"message": err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = h.notify(result.OutTradeNo, result.TradeId) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		c.String(http.StatusOK, "fail") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.String(http.StatusOK, "success") | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user