mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 00:03:48 +08:00 
			
		
		
		
	Compare commits
	
		
			1881 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					58e99976b3 | ||
| 
						 | 
					567803ebeb | ||
| 
						 | 
					8e3d67cc7f | ||
| 
						 | 
					b463608ab7 | ||
| 
						 | 
					eefd36562d | ||
| 
						 | 
					ebf4497b34 | ||
| 
						 | 
					194b0343bf | ||
| 
						 | 
					adb1dfbded | ||
| 
						 | 
					9e689f6022 | ||
| 
						 | 
					499f5c915f | ||
| 
						 | 
					a9f11d79a9 | ||
| 
						 | 
					db5e9ea2d0 | ||
| 
						 | 
					31fbf7e48a | ||
| 
						 | 
					e2e24078c5 | ||
| 
						 | 
					39ef92e1ce | ||
| 
						 | 
					f63ba187dd | ||
| 
						 | 
					23d1ac1021 | ||
| 
						 | 
					f08329eef4 | ||
| 
						 | 
					e993a582cd | ||
| 
						 | 
					1524e189dc | ||
| 
						 | 
					2b5165324c | ||
| 
						 | 
					8f67826072 | ||
| 
						 | 
					ac9a31f049 | ||
| 
						 | 
					ed2cdbbc31 | ||
| 
						 | 
					8bd2feaf7e | ||
| 
						 | 
					4eccfe6d2c | ||
| 
						 | 
					5ba2e000a1 | ||
| 
						 | 
					fe9adadb3a | ||
| 
						 | 
					805c2a045e | ||
| 
						 | 
					fd7b196134 | ||
| 
						 | 
					1b1c327c35 | ||
| 
						 | 
					a228f603d2 | ||
| 
						 | 
					7d0a05ee11 | ||
| 
						 | 
					5f0a62b63e | ||
| 
						 | 
					825a1b1027 | ||
| 
						 | 
					38dde1a373 | ||
| 
						 | 
					950e7d1b00 | ||
| 
						 | 
					89ded74345 | ||
| 
						 | 
					d8f9f48278 | ||
| 
						 | 
					818bb38617 | ||
| 
						 | 
					bdd76addf3 | ||
| 
						 | 
					0202d75ff4 | ||
| 
						 | 
					2936f21f12 | ||
| 
						 | 
					a0831ec6cd | ||
| 
						 | 
					705ca4d20a | ||
| 
						 | 
					2926aab7a4 | ||
| 
						 | 
					6a066f1b8e | ||
| 
						 | 
					775fb1f853 | ||
| 
						 | 
					b7d137247a | ||
| 
						 | 
					22278c40bf | ||
| 
						 | 
					4373642ebd | ||
| 
						 | 
					e29b32bd2d | ||
| 
						 | 
					5bf90920f5 | ||
| 
						 | 
					fa6bb5e46a | ||
| 
						 | 
					edcbb3e226 | ||
| 
						 | 
					cebf8497a4 | ||
| 
						 | 
					5213bdf08b | ||
| 
						 | 
					cded4bdcc7 | ||
| 
						 | 
					cf817fd8ea | ||
| 
						 | 
					596b700d63 | ||
| 
						 | 
					a2481ff1cf | ||
| 
						 | 
					59dfa95bf8 | ||
| 
						 | 
					488169683f | ||
| 
						 | 
					2ba3c52e6e | ||
| 
						 | 
					18179613fc | ||
| 
						 | 
					8af0fec8ec | ||
| 
						 | 
					acee2d9d81 | ||
| 
						 | 
					cbf06eea24 | ||
| 
						 | 
					989b4a64d6 | ||
| 
						 | 
					b01b10014a | ||
| 
						 | 
					e857f98e5c | ||
| 
						 | 
					274cff71b1 | ||
| 
						 | 
					06573c5d12 | ||
| 
						 | 
					937e5befa2 | ||
| 
						 | 
					fb403bde8b | ||
| 
						 | 
					ba174ef3ee | ||
| 
						 | 
					b7b702862f | ||
| 
						 | 
					6df2b5735b | ||
| 
						 | 
					130e151a06 | ||
| 
						 | 
					ab903e3cc1 | ||
| 
						 | 
					bc7d06d3e5 | ||
| 
						 | 
					f6b5a94b29 | ||
| 
						 | 
					237387b2ab | ||
| 
						 | 
					0c1f650e9c | ||
| 
						 | 
					357c77ef30 | ||
| 
						 | 
					dc7c049a7b | ||
| 
						 | 
					8e81dfa12a | ||
| 
						 | 
					188fb23f08 | ||
| 
						 | 
					0ff76f0f21 | ||
| 
						 | 
					5347a12035 | ||
| 
						 | 
					787caa84c8 | ||
| 
						 | 
					691d453f41 | ||
| 
						 | 
					c2503e663a | ||
| 
						 | 
					5b7f2603ae | ||
| 
						 | 
					710b008453 | ||
| 
						 | 
					6e7aecc568 | ||
| 
						 | 
					405a88862b | ||
| 
						 | 
					3fd7f810f0 | ||
| 
						 | 
					296eabe09a | ||
| 
						 | 
					e04a48623d | ||
| 
						 | 
					b68f7e3fd1 | ||
| 
						 | 
					d30d5585c6 | ||
| 
						 | 
					2e1bad387c | ||
| 
						 | 
					1b7c7a0dc1 | ||
| 
						 | 
					207f2b5ac4 | ||
| 
						 | 
					d13fa1392f | ||
| 
						 | 
					9bf886fe98 | ||
| 
						 | 
					aeef77ac24 | ||
| 
						 | 
					9a97a1ee72 | ||
| 
						 | 
					54b45ec2ff | ||
| 
						 | 
					5c18a50330 | ||
| 
						 | 
					c434f85045 | ||
| 
						 | 
					d78d3ffe02 | ||
| 
						 | 
					4d10279870 | ||
| 
						 | 
					44240f65d5 | ||
| 
						 | 
					6aaf607ed7 | ||
| 
						 | 
					97e81a7dcc | ||
| 
						 | 
					cff0397735 | ||
| 
						 | 
					9e8f1ed6bf | ||
| 
						 | 
					2aa0b51c09 | ||
| 
						 | 
					4dbfdab50d | ||
| 
						 | 
					ce8a2d0222 | ||
| 
						 | 
					c39814ce2b | ||
| 
						 | 
					135755d21d | ||
| 
						 | 
					95a071014c | ||
| 
						 | 
					5be4e83876 | ||
| 
						 | 
					6e03f4b363 | ||
| 
						 | 
					9de9489673 | ||
| 
						 | 
					bb8644dea0 | ||
| 
						 | 
					cbc9eb3a59 | ||
| 
						 | 
					ff9142ddd4 | ||
| 
						 | 
					0593359ef8 | ||
| 
						 | 
					9814fec930 | ||
| 
						 | 
					35f469fb82 | ||
| 
						 | 
					53ba731159 | ||
| 
						 | 
					705ad58a8f | ||
| 
						 | 
					2081d3ce29 | ||
| 
						 | 
					41d9c097e8 | ||
| 
						 | 
					c96f86fbbf | ||
| 
						 | 
					1fe1e40a43 | ||
| 
						 | 
					4b3b64e9e2 | ||
| 
						 | 
					ad6e2dd370 | ||
| 
						 | 
					2f2e146951 | ||
| 
						 | 
					bb63f23414 | ||
| 
						 | 
					a09c529414 | ||
| 
						 | 
					43f6bf74f2 | ||
| 
						 | 
					a8d1d58e95 | ||
| 
						 | 
					662d7b099e | ||
| 
						 | 
					6695be815e | ||
| 
						 | 
					d5eeeea764 | ||
| 
						 | 
					fbd3478772 | ||
| 
						 | 
					43c507c597 | ||
| 
						 | 
					2102e1afbb | ||
| 
						 | 
					e356771049 | ||
| 
						 | 
					155c56f502 | ||
| 
						 | 
					48139290ed | ||
| 
						 | 
					37a9b0e485 | ||
| 
						 | 
					bd852c82b7 | ||
| 
						 | 
					52a3f13f1e | ||
| 
						 | 
					13564993d7 | ||
| 
						 | 
					a678a11c33 | ||
| 
						 | 
					bfc1e1bc2c | ||
| 
						 | 
					d34b785238 | ||
| 
						 | 
					ba20717a09 | ||
| 
						 | 
					52e40daf23 | ||
| 
						 | 
					1086e1d631 | ||
| 
						 | 
					b2f57aa483 | ||
| 
						 | 
					3094b9c8fd | ||
| 
						 | 
					4c2dba1004 | ||
| 
						 | 
					0f0a4c0a7e | ||
| 
						 | 
					430a7b2297 | ||
| 
						 | 
					c374126f69 | ||
| 
						 | 
					c91a38a882 | ||
| 
						 | 
					8498cd71dc | ||
| 
						 | 
					6e02bee4b7 | ||
| 
						 | 
					bf30517393 | ||
| 
						 | 
					b62218110e | ||
| 
						 | 
					495f86e7fc | ||
| 
						 | 
					e2960b2607 | ||
| 
						 | 
					471017657f | ||
| 
						 | 
					88e7c39066 | ||
| 
						 | 
					4861dd75be | ||
| 
						 | 
					2a6dd636fa | ||
| 
						 | 
					342ceea371 | ||
| 
						 | 
					6bf38f78d5 | ||
| 
						 | 
					880c34dfee | ||
| 
						 | 
					5a04a935be | ||
| 
						 | 
					a1e487100d | ||
| 
						 | 
					8923e938d2 | ||
| 
						 | 
					77948b1e16 | ||
| 
						 | 
					1a1734abf0 | ||
| 
						 | 
					e28a12a1ee | ||
| 
						 | 
					8093a3eeb2 | ||
| 
						 | 
					00a8bc6784 | ||
| 
						 | 
					9edb3d0a82 | ||
| 
						 | 
					8fffa60569 | ||
| 
						 | 
					d95fab11be | ||
| 
						 | 
					2debe7e927 | ||
| 
						 | 
					6ef09c8ad5 | ||
| 
						 | 
					478bc32ddd | ||
| 
						 | 
					283a023a06 | ||
| 
						 | 
					dfd2be1265 | ||
| 
						 | 
					d315edef5f | ||
| 
						 | 
					33ff2e29e5 | ||
| 
						 | 
					5fa17b300e | ||
| 
						 | 
					4df1c7a136 | ||
| 
						 | 
					32919de7a7 | ||
| 
						 | 
					c522248d39 | ||
| 
						 | 
					7d126aab41 | ||
| 
						 | 
					246d96ee63 | ||
| 
						 | 
					16ac57ced3 | ||
| 
						 | 
					909ae4aa00 | ||
| 
						 | 
					4976b967e7 | ||
| 
						 | 
					26e3ababcf | ||
| 
						 | 
					e874178782 | ||
| 
						 | 
					4d9f89f630 | ||
| 
						 | 
					8cb66ad01b | ||
| 
						 | 
					b989199edb | ||
| 
						 | 
					f887a39912 | ||
| 
						 | 
					0d81fe6c8e | ||
| 
						 | 
					2beffd3dd3 | ||
| 
						 | 
					be45f41e34 | ||
| 
						 | 
					d8cb92d8d4 | ||
| 
						 | 
					e9ac58b1ef | ||
| 
						 | 
					158db83965 | ||
| 
						 | 
					59d9ae96ac | ||
| 
						 | 
					603bfa7def | ||
| 
						 | 
					3cff6f7189 | ||
| 
						 | 
					829fb879a6 | ||
| 
						 | 
					14aee28289 | ||
| 
						 | 
					0385e60ce1 | ||
| 
						 | 
					8c1b4d4516 | ||
| 
						 | 
					aaea23f785 | ||
| 
						 | 
					068df8fa15 | ||
| 
						 | 
					131efd6ba5 | ||
| 
						 | 
					e371310d02 | ||
| 
						 | 
					866564370d | ||
| 
						 | 
					96b8121210 | ||
| 
						 | 
					dcdc0d8918 | ||
| 
						 | 
					1960a85ead | ||
| 
						 | 
					6c7fa17e50 | ||
| 
						 | 
					69cb479e01 | ||
| 
						 | 
					38a0d00142 | ||
| 
						 | 
					db90131bd6 | ||
| 
						 | 
					5c77e67b0f | ||
| 
						 | 
					c18d272413 | ||
| 
						 | 
					961cee5e41 | ||
| 
						 | 
					cd0952e170 | ||
| 
						 | 
					c9cc93be8c | ||
| 
						 | 
					e6a6d0485d | ||
| 
						 | 
					49f2e1a71e | ||
| 
						 | 
					a642ae0332 | ||
| 
						 | 
					97eff6085a | ||
| 
						 | 
					67f508c7da | ||
| 
						 | 
					8b2e2d61af | ||
| 
						 | 
					2225f09797 | ||
| 
						 | 
					c096efb416 | ||
| 
						 | 
					fe7fa46a0c | ||
| 
						 | 
					cdaf6fb9dc | ||
| 
						 | 
					6d8901fa21 | ||
| 
						 | 
					78f443ed6d | ||
| 
						 | 
					7750cb0021 | ||
| 
						 | 
					54e8d72b10 | ||
| 
						 | 
					327d68a347 | ||
| 
						 | 
					05161f48fd | ||
| 
						 | 
					7c272b0efd | ||
| 
						 | 
					e971bf6b88 | ||
| 
						 | 
					5eb4ed157e | ||
| 
						 | 
					55b979784c | ||
| 
						 | 
					d4e00b960c | ||
| 
						 | 
					79adc871ef | ||
| 
						 | 
					af0eff88b8 | ||
| 
						 | 
					97aa922b5f | ||
| 
						 | 
					c755befa28 | ||
| 
						 | 
					11c760a4e8 | ||
| 
						 | 
					3f6890da27 | ||
| 
						 | 
					8144fada25 | ||
| 
						 | 
					08319391ea | ||
| 
						 | 
					87b03332d9 | ||
| 
						 | 
					ae7f1c03e2 | ||
| 
						 | 
					8b14eeadf4 | ||
| 
						 | 
					151017ed47 | ||
| 
						 | 
					e0ead127e0 | ||
| 
						 | 
					941d15b635 | ||
| 
						 | 
					0887bcdee0 | ||
| 
						 | 
					d8058400dd | ||
| 
						 | 
					67d83041d7 | ||
| 
						 | 
					5a1ed953de | ||
| 
						 | 
					1350f388f0 | ||
| 
						 | 
					58d127b602 | ||
| 
						 | 
					65dde9e69d | ||
| 
						 | 
					13bf73e6cf | ||
| 
						 | 
					2e5bd238b7 | ||
| 
						 | 
					aa46fdb113 | ||
| 
						 | 
					8fc8fd6cba | ||
| 
						 | 
					4a09da3a25 | ||
| 
						 | 
					dfc6c87250 | ||
| 
						 | 
					d70fcbb9f3 | ||
| 
						 | 
					b63e01225e | ||
| 
						 | 
					a16ef6476d | ||
| 
						 | 
					561b82027a | ||
| 
						 | 
					dae75343d0 | ||
| 
						 | 
					f6d8fbf570 | ||
| 
						 | 
					d7564127d2 | ||
| 
						 | 
					568201ebbb | ||
| 
						 | 
					c6bf54fd9d | ||
| 
						 | 
					ab421f2185 | ||
| 
						 | 
					8d47072e1c | ||
| 
						 | 
					f71a2f5263 | ||
| 
						 | 
					d601226187 | ||
| 
						 | 
					d000cc5a67 | ||
| 
						 | 
					4801832d9d | ||
| 
						 | 
					04d6ba0853 | ||
| 
						 | 
					6b0f42d0b8 | ||
| 
						 | 
					8d7c028ca8 | ||
| 
						 | 
					08379c21ac | ||
| 
						 | 
					3ae7ebfeaf | ||
| 
						 | 
					2499bd9ad3 | ||
| 
						 | 
					aa42d38387 | ||
| 
						 | 
					ad6da0d320 | ||
| 
						 | 
					43843b92f2 | ||
| 
						 | 
					a2583c0591 | ||
| 
						 | 
					5da879600a | ||
| 
						 | 
					0ea4a29152 | ||
| 
						 | 
					87ed2064e3 | ||
| 
						 | 
					7fe50788a5 | ||
| 
						 | 
					34e96e91d4 | ||
| 
						 | 
					c4a68076d6 | ||
| 
						 | 
					8c4c2b89ce | ||
| 
						 | 
					9e365895af | ||
| 
						 | 
					373021c191 | ||
| 
						 | 
					9ece25e17d | ||
| 
						 | 
					740c3c1b00 | ||
| 
						 | 
					2140eff51b | ||
| 
						 | 
					67c7132e6b | ||
| 
						 | 
					dfba3c30a5 | ||
| 
						 | 
					c77843424b | ||
| 
						 | 
					4b717109d2 | ||
| 
						 | 
					2d4959aa7d | ||
| 
						 | 
					e54e908fbc | ||
| 
						 | 
					167c59a159 | ||
| 
						 | 
					7a11a9ef15 | ||
| 
						 | 
					1d0006ce59 | ||
| 
						 | 
					c9d0700fd9 | ||
| 
						 | 
					6a8b4ee2f1 | ||
| 
						 | 
					f9b809801d | ||
| 
						 | 
					72b1515b68 | ||
| 
						 | 
					cc551ba266 | ||
| 
						 | 
					754ba02263 | ||
| 
						 | 
					23787ff462 | ||
| 
						 | 
					7ddf57ae06 | ||
| 
						 | 
					9f603c5e77 | ||
| 
						 | 
					3f0252b498 | ||
| 
						 | 
					3d720bdd81 | ||
| 
						 | 
					cc5180a6f7 | ||
| 
						 | 
					a8eccbe43b | ||
| 
						 | 
					1d9d487f0e | ||
| 
						 | 
					2ac77ac39f | ||
| 
						 | 
					96f1126d02 | ||
| 
						 | 
					4eaa518cf3 | ||
| 
						 | 
					7f9b8d8246 | ||
| 
						 | 
					5622f94fc8 | ||
| 
						 | 
					5132d52a44 | ||
| 
						 | 
					95457b7dcd | ||
| 
						 | 
					1bcbf74883 | ||
| 
						 | 
					6a9de72c78 | ||
| 
						 | 
					abdf5298fe | ||
| 
						 | 
					088a614160 | ||
| 
						 | 
					2129f7a8b7 | ||
| 
						 | 
					013ee98f53 | ||
| 
						 | 
					f6f8748521 | ||
| 
						 | 
					dc46ed0e05 | ||
| 
						 | 
					59301df073 | ||
| 
						 | 
					2a0c657ca3 | ||
| 
						 | 
					e17dcf4d5f | ||
| 
						 | 
					a0aee80c63 | ||
| 
						 | 
					09f44e6d9b | ||
| 
						 | 
					4910be3403 | ||
| 
						 | 
					59824bffc5 | ||
| 
						 | 
					1541d74c84 | ||
| 
						 | 
					cb0dacd5e0 | ||
| 
						 | 
					34c9151dc1 | ||
| 
						 | 
					7463cfc66c | ||
| 
						 | 
					6c69770ed6 | ||
| 
						 | 
					b248560ba2 | ||
| 
						 | 
					5b7c38c67f | ||
| 
						 | 
					37368fe13f | ||
| 
						 | 
					b7bec8ecb7 | ||
| 
						 | 
					246b023624 | ||
| 
						 | 
					82fe9c3596 | ||
| 
						 | 
					9f44c34d34 | ||
| 
						 | 
					afd84516b0 | ||
| 
						 | 
					a6b9f57a50 | ||
| 
						 | 
					024f00b73c | ||
| 
						 | 
					42bc23cacf | ||
| 
						 | 
					24a21bf2ee | ||
| 
						 | 
					282f55c7a3 | ||
| 
						 | 
					f22c6bf658 | ||
| 
						 | 
					44798f89ba | ||
| 
						 | 
					0df700ec18 | ||
| 
						 | 
					596cb2b206 | ||
| 
						 | 
					46141f87b8 | ||
| 
						 | 
					d1965deff1 | ||
| 
						 | 
					eecce10018 | ||
| 
						 | 
					b793b81768 | ||
| 
						 | 
					c8506e3d3b | ||
| 
						 | 
					a5ef4299ec | ||
| 
						 | 
					bddd611cc1 | ||
| 
						 | 
					cdb1a8bde1 | ||
| 
						 | 
					b399fc557a | ||
| 
						 | 
					233f6e00f0 | ||
| 
						 | 
					b20ea734fd | ||
| 
						 | 
					b7dba68549 | ||
| 
						 | 
					7ab0acd6a4 | ||
| 
						 | 
					64e5fc48ba | ||
| 
						 | 
					1d60ccc516 | ||
| 
						 | 
					a692cf1338 | ||
| 
						 | 
					2a8d2213b1 | ||
| 
						 | 
					6998dd7af4 | ||
| 
						 | 
					a27ce36a32 | ||
| 
						 | 
					9343c73e0f | ||
| 
						 | 
					3fdcc895ed | ||
| 
						 | 
					739cd46539 | ||
| 
						 | 
					88e510d07a | ||
| 
						 | 
					f8fed83507 | ||
| 
						 | 
					2526feb0d9 | ||
| 
						 | 
					d63536d5ef | ||
| 
						 | 
					4171b2bdfb | ||
| 
						 | 
					4905fb28d4 | ||
| 
						 | 
					0611f8de6c | ||
| 
						 | 
					a3a2a8abcb | ||
| 
						 | 
					df9e495dc5 | ||
| 
						 | 
					839dd8dbf4 | ||
| 
						 | 
					8a48476105 | ||
| 
						 | 
					0375164f40 | ||
| 
						 | 
					9d4a1705a5 | ||
| 
						 | 
					691294b444 | ||
| 
						 | 
					871e5733c7 | ||
| 
						 | 
					bdea12c51a | ||
| 
						 | 
					e8a663e3c7 | ||
| 
						 | 
					a27d9ea259 | ||
| 
						 | 
					422d439627 | ||
| 
						 | 
					7cd824c284 | ||
| 
						 | 
					edeff3c169 | ||
| 
						 | 
					e27d95e2b5 | ||
| 
						 | 
					13076421a1 | ||
| 
						 | 
					c24b4d7074 | ||
| 
						 | 
					3943e7f05f | ||
| 
						 | 
					6839827db0 | ||
| 
						 | 
					cd0d9dad98 | ||
| 
						 | 
					ab24398748 | ||
| 
						 | 
					857da34b9c | ||
| 
						 | 
					6110522b54 | ||
| 
						 | 
					ccad7e7bb5 | ||
| 
						 | 
					bcdf5e3776 | ||
| 
						 | 
					b8de15d66e | ||
| 
						 | 
					2207830db9 | ||
| 
						 | 
					c02661ea29 | ||
| 
						 | 
					d52dfbfef4 | ||
| 
						 | 
					3c70c8ae59 | ||
| 
						 | 
					d6a04f96fe | ||
| 
						 | 
					b985b62068 | ||
| 
						 | 
					66ccb387e8 | ||
| 
						 | 
					297b760293 | ||
| 
						 | 
					5f820b9dc1 | ||
| 
						 | 
					088abfe7ab | ||
| 
						 | 
					3cc2263dc7 | ||
| 
						 | 
					17340ae0ac | ||
| 
						 | 
					f0a3c5d8ae | ||
| 
						 | 
					fe687af8ca | ||
| 
						 | 
					2a4ef27774 | ||
| 
						 | 
					7e26029268 | ||
| 
						 | 
					2b057f32aa | ||
| 
						 | 
					03f7f2a53e | ||
| 
						 | 
					bc6451026f | ||
| 
						 | 
					296260bf6a | ||
| 
						 | 
					99fd596862 | ||
| 
						 | 
					dd07acd21a | ||
| 
						 | 
					f0959b5df6 | ||
| 
						 | 
					89f6402fbf | ||
| 
						 | 
					6788edbe9d | ||
| 
						 | 
					853059c814 | ||
| 
						 | 
					3895305882 | ||
| 
						 | 
					5a0876f8dc | ||
| 
						 | 
					1b0938b33f | ||
| 
						 | 
					2e32238652 | ||
| 
						 | 
					c2acbaaa94 | ||
| 
						 | 
					ad1a99aa44 | ||
| 
						 | 
					02faff461a | ||
| 
						 | 
					24e4be019a | ||
| 
						 | 
					e18e5a38c6 | ||
| 
						 | 
					13c917ad7e | ||
| 
						 | 
					2f9b1b7835 | ||
| 
						 | 
					0a8cf6870f | ||
| 
						 | 
					717b137a6d | ||
| 
						 | 
					061291cebb | ||
| 
						 | 
					f755bdccae | ||
| 
						 | 
					b8e0d7760b | ||
| 
						 | 
					4bba77ab47 | ||
| 
						 | 
					962de0183c | ||
| 
						 | 
					6944a32ff3 | ||
| 
						 | 
					627396dbf7 | ||
| 
						 | 
					5742b40aee | ||
| 
						 | 
					6c300bb018 | ||
| 
						 | 
					7f1ec90748 | ||
| 
						 | 
					9273df4af2 | ||
| 
						 | 
					4a99be2f15 | ||
| 
						 | 
					669d784f54 | ||
| 
						 | 
					bee19392c1 | ||
| 
						 | 
					5253d657b6 | ||
| 
						 | 
					27c816cf3b | ||
| 
						 | 
					c2548c5007 | ||
| 
						 | 
					0d81776212 | ||
| 
						 | 
					bdec823148 | ||
| 
						 | 
					00d31a2379 | ||
| 
						 | 
					a6de958daa | ||
| 
						 | 
					cccab31c0f | ||
| 
						 | 
					50c25d4574 | ||
| 
						 | 
					5d65505ab7 | ||
| 
						 | 
					68100f7f24 | ||
| 
						 | 
					3dc7d0516a | ||
| 
						 | 
					38777ea285 | ||
| 
						 | 
					50335ebc2d | ||
| 
						 | 
					00c8f08179 | ||
| 
						 | 
					bcadee7290 | ||
| 
						 | 
					45d6579fb6 | ||
| 
						 | 
					cac3194d5b | ||
| 
						 | 
					dad9254128 | ||
| 
						 | 
					4ddf3bf2bf | ||
| 
						 | 
					9934143f00 | ||
| 
						 | 
					d45f9fbad6 | ||
| 
						 | 
					f617bde81b | ||
| 
						 | 
					d98b08d7cd | ||
| 
						 | 
					b2771b7b3f | ||
| 
						 | 
					5a8fe5a6cf | ||
| 
						 | 
					44b3237dd6 | ||
| 
						 | 
					36c27d6092 | ||
| 
						 | 
					b987a2d365 | ||
| 
						 | 
					3ab29da8f0 | ||
| 
						 | 
					1cf299f090 | ||
| 
						 | 
					3699f024f1 | ||
| 
						 | 
					c87d78c666 | ||
| 
						 | 
					3d37a3d367 | ||
| 
						 | 
					79c8d90049 | ||
| 
						 | 
					73d8236697 | ||
| 
						 | 
					1afc3e5e1c | ||
| 
						 | 
					114d0088dc | ||
| 
						 | 
					87e8bbfa41 | ||
| 
						 | 
					43b6665370 | ||
| 
						 | 
					3e3d3e162e | ||
| 
						 | 
					5fb9f84182 | ||
| 
						 | 
					cc80b87fee | ||
| 
						 | 
					e35c34ad9a | ||
| 
						 | 
					b614ecccc2 | ||
| 
						 | 
					1a4d798f8b | ||
| 
						 | 
					8251c6589b | ||
| 
						 | 
					afb91a7023 | ||
| 
						 | 
					4edf2ea02a | ||
| 
						 | 
					dc4c1f7877 | ||
| 
						 | 
					12dd4659cd | ||
| 
						 | 
					bbc8fe2b40 | ||
| 
						 | 
					5d4f40c004 | ||
| 
						 | 
					3c34e8e0e7 | ||
| 
						 | 
					7441a3a717 | ||
| 
						 | 
					57c932f07c | ||
| 
						 | 
					3eef9a836b | ||
| 
						 | 
					922202734a | ||
| 
						 | 
					fffd7e375f | ||
| 
						 | 
					8b3b0139b0 | ||
| 
						 | 
					d87f09c957 | ||
| 
						 | 
					31828a3336 | ||
| 
						 | 
					b29884f8df | ||
| 
						 | 
					b270960a04 | ||
| 
						 | 
					c0ee2c2d8e | ||
| 
						 | 
					5c4899df6e | ||
| 
						 | 
					be1580e949 | ||
| 
						 | 
					9a797bb4a5 | ||
| 
						 | 
					6424eb871c | ||
| 
						 | 
					b0c9ffc5a6 | ||
| 
						 | 
					8a8c43c7a5 | ||
| 
						 | 
					f527cc5b98 | ||
| 
						 | 
					9a503ddc72 | ||
| 
						 | 
					debe8dc209 | ||
| 
						 | 
					b130466c8f | ||
| 
						 | 
					2f0215ac87 | ||
| 
						 | 
					3ab5459778 | ||
| 
						 | 
					dd5cc206e5 | ||
| 
						 | 
					4eb1bf2de2 | ||
| 
						 | 
					142cd553a3 | ||
| 
						 | 
					6c47551b03 | ||
| 
						 | 
					657ecccee3 | ||
| 
						 | 
					b4ba70c0f3 | ||
| 
						 | 
					1232c3cd9c | ||
| 
						 | 
					c2bf5e845a | ||
| 
						 | 
					3ac04a3938 | ||
| 
						 | 
					8900e72e45 | ||
| 
						 | 
					b7abc42209 | ||
| 
						 | 
					0cb192135d | ||
| 
						 | 
					a48179ce0e | ||
| 
						 | 
					c1ca687630 | ||
| 
						 | 
					e589f25a05 | ||
| 
						 | 
					29cc24508b | ||
| 
						 | 
					cc1a3ce343 | ||
| 
						 | 
					3db55cbd48 | ||
| 
						 | 
					7bb76d581c | ||
| 
						 | 
					c0a7f41747 | ||
| 
						 | 
					0d733c0be0 | ||
| 
						 | 
					1ba08bbfa6 | ||
| 
						 | 
					8b40ac5b5c | ||
| 
						 | 
					51a8f42d89 | ||
| 
						 | 
					24479814e9 | ||
| 
						 | 
					3b081ff0f4 | ||
| 
						 | 
					99df028237 | ||
| 
						 | 
					7f31a301e3 | ||
| 
						 | 
					b354b88876 | ||
| 
						 | 
					039d949523 | ||
| 
						 | 
					5e0be4d10e | ||
| 
						 | 
					5b16dc6ee7 | ||
| 
						 | 
					468b48151f | ||
| 
						 | 
					34648c704f | ||
| 
						 | 
					fa5c036041 | ||
| 
						 | 
					1966e69460 | ||
| 
						 | 
					0fdc588167 | ||
| 
						 | 
					74b1b07b31 | ||
| 
						 | 
					2e023cb8dc | ||
| 
						 | 
					118c0e1ff1 | ||
| 
						 | 
					e933f32d9c | ||
| 
						 | 
					81d075c028 | ||
| 
						 | 
					bd4b0c4d65 | ||
| 
						 | 
					514dd6c76a | ||
| 
						 | 
					0b2501c1d8 | ||
| 
						 | 
					ddc323a8c3 | ||
| 
						 | 
					9d28e62142 | ||
| 
						 | 
					2933c057a2 | ||
| 
						 | 
					c1d892069e | ||
| 
						 | 
					20ed6d1d3d | ||
| 
						 | 
					61b2dbc9f1 | ||
| 
						 | 
					90f5275201 | ||
| 
						 | 
					be3245666e | ||
| 
						 | 
					fe64b203da | ||
| 
						 | 
					dacdd6fe74 | ||
| 
						 | 
					9eb1353455 | ||
| 
						 | 
					6807f7e88a | ||
| 
						 | 
					8a1c55c731 | ||
| 
						 | 
					087f5ab2d1 | ||
| 
						 | 
					9059bebd07 | ||
| 
						 | 
					47c5a0387b | ||
| 
						 | 
					d18f79fe74 | ||
| 
						 | 
					f9da18ad52 | ||
| 
						 | 
					3a8f0be28a | ||
| 
						 | 
					5c9025ca22 | ||
| 
						 | 
					60cf380f96 | ||
| 
						 | 
					d02cb573fd | ||
| 
						 | 
					ab8240613e | ||
| 
						 | 
					caa538a1d0 | ||
| 
						 | 
					d6e9dc6839 | ||
| 
						 | 
					b584b4bfb6 | ||
| 
						 | 
					6891bdde53 | ||
| 
						 | 
					bda335212d | ||
| 
						 | 
					ecadfeab19 | ||
| 
						 | 
					06f4cdc649 | ||
| 
						 | 
					a38d547200 | ||
| 
						 | 
					336a7d5b56 | ||
| 
						 | 
					a15f431a7f | ||
| 
						 | 
					a0f464830f | ||
| 
						 | 
					3bb5b66e8b | ||
| 
						 | 
					9bf7fa4081 | ||
| 
						 | 
					d9ff3242c6 | ||
| 
						 | 
					96ead65774 | ||
| 
						 | 
					cd562ab8ed | ||
| 
						 | 
					7ad41927aa | ||
| 
						 | 
					3a8a69ac2e | ||
| 
						 | 
					4ca9dfd9c0 | ||
| 
						 | 
					0d5b4936bd | ||
| 
						 | 
					8a9f386d8f | ||
| 
						 | 
					b5f6eaf159 | ||
| 
						 | 
					adfee8bf58 | ||
| 
						 | 
					25c80921e0 | ||
| 
						 | 
					fbfa2a71a9 | ||
| 
						 | 
					5c51ed48c3 | ||
| 
						 | 
					9a1368ef17 | ||
| 
						 | 
					22febfc42a | ||
| 
						 | 
					31b02b97d3 | ||
| 
						 | 
					de482dd9f4 | ||
| 
						 | 
					42da38c5c3 | ||
| 
						 | 
					74af67a41f | ||
| 
						 | 
					0a01b55713 | ||
| 
						 | 
					170b41441b | ||
| 
						 | 
					3b292c2a12 | ||
| 
						 | 
					4991e50e16 | ||
| 
						 | 
					db0ba0d9a0 | ||
| 
						 | 
					1f236ef929 | ||
| 
						 | 
					3a23ff6b42 | ||
| 
						 | 
					7f86c5984d | ||
| 
						 | 
					1e9c5adb0a | ||
| 
						 | 
					6fcee49b19 | ||
| 
						 | 
					abab76ccc6 | ||
| 
						 | 
					ac527851a0 | ||
| 
						 | 
					6efd92806f | ||
| 
						 | 
					64bbeaedcb | ||
| 
						 | 
					cfe333e89f | ||
| 
						 | 
					3e984a1bcb | ||
| 
						 | 
					a7237fe62f | ||
| 
						 | 
					79165214d7 | ||
| 
						 | 
					c3c454b7d7 | ||
| 
						 | 
					3f7855dd6e | ||
| 
						 | 
					d4d708d44b | ||
| 
						 | 
					f0610281e5 | ||
| 
						 | 
					7f0b6a3a46 | ||
| 
						 | 
					e87e7e2164 | ||
| 
						 | 
					c2a7c089d2 | ||
| 
						 | 
					1a3ad390fd | ||
| 
						 | 
					df5bd4df60 | ||
| 
						 | 
					c383367e9f | ||
| 
						 | 
					79b6010104 | ||
| 
						 | 
					7a9f45c96b | ||
| 
						 | 
					97b0a98793 | ||
| 
						 | 
					7277cb289f | ||
| 
						 | 
					5230f90540 | ||
| 
						 | 
					86b2d5eff2 | ||
| 
						 | 
					803db4e895 | ||
| 
						 | 
					7873847616 | ||
| 
						 | 
					7cee9f2ebb | ||
| 
						 | 
					5e46f149d9 | ||
| 
						 | 
					8be9a21efd | ||
| 
						 | 
					574fc52332 | ||
| 
						 | 
					6a3e26b566 | ||
| 
						 | 
					8c924ca98f | ||
| 
						 | 
					0355c37bef | ||
| 
						 | 
					a9adc0c1a2 | ||
| 
						 | 
					9b7ee538c4 | ||
| 
						 | 
					ed286c8894 | ||
| 
						 | 
					d900a3d08e | ||
| 
						 | 
					a55da15202 | ||
| 
						 | 
					cdf5b66729 | ||
| 
						 | 
					9728118f94 | ||
| 
						 | 
					1cff4b63cd | ||
| 
						 | 
					9cc2ea412b | ||
| 
						 | 
					da14309ef9 | ||
| 
						 | 
					3f1ad4b7dc | ||
| 
						 | 
					fbb216fe3b | ||
| 
						 | 
					56c225bf20 | ||
| 
						 | 
					95efbd5659 | ||
| 
						 | 
					8b68bfb28c | ||
| 
						 | 
					4596c1049c | ||
| 
						 | 
					a5299b52d4 | ||
| 
						 | 
					b35d95f0c7 | ||
| 
						 | 
					9e9bc52737 | ||
| 
						 | 
					01419df998 | ||
| 
						 | 
					76106e4504 | ||
| 
						 | 
					a6c00c42fa | ||
| 
						 | 
					4e23005b5b | ||
| 
						 | 
					4cc9db7115 | ||
| 
						 | 
					f27e2de220 | ||
| 
						 | 
					4f1ed54059 | ||
| 
						 | 
					eefcabcff3 | ||
| 
						 | 
					8227a73e35 | ||
| 
						 | 
					63e4669e3f | ||
| 
						 | 
					adfd8c1939 | ||
| 
						 | 
					7d5428f775 | ||
| 
						 | 
					8eed7ff534 | ||
| 
						 | 
					ab3c4562fa | ||
| 
						 | 
					c79c4e74d0 | ||
| 
						 | 
					13f89bf335 | ||
| 
						 | 
					f1855fd0a1 | ||
| 
						 | 
					e6a18445c3 | ||
| 
						 | 
					1f964c74e9 | ||
| 
						 | 
					c8ab209426 | ||
| 
						 | 
					4fb2c5803c | ||
| 
						 | 
					360fea4085 | ||
| 
						 | 
					b5947545cb | ||
| 
						 | 
					9794d67eaa | ||
| 
						 | 
					342b76f666 | ||
| 
						 | 
					b60a639312 | ||
| 
						 | 
					49b5906bc7 | ||
| 
						 | 
					870706c4ff | ||
| 
						 | 
					3075bfb7fc | ||
| 
						 | 
					491471fa10 | ||
| 
						 | 
					82e06fad33 | ||
| 
						 | 
					6dbf61d4e4 | ||
| 
						 | 
					4a9028747b | ||
| 
						 | 
					81545d192b | ||
| 
						 | 
					4a8ff0ccf0 | ||
| 
						 | 
					37cdc26160 | ||
| 
						 | 
					99341f0484 | ||
| 
						 | 
					fb6a35ebe4 | ||
| 
						 | 
					f58ac29ad0 | ||
| 
						 | 
					f7a565bb80 | ||
| 
						 | 
					7060edb3e5 | ||
| 
						 | 
					bf5e72b7e0 | ||
| 
						 | 
					41ae411f9b | ||
| 
						 | 
					ec6186596d | ||
| 
						 | 
					79b7fee47c | ||
| 
						 | 
					dece19cec6 | ||
| 
						 | 
					0044bf10af | ||
| 
						 | 
					6aa1d27711 | ||
| 
						 | 
					e9348d3611 | ||
| 
						 | 
					264e77f383 | ||
| 
						 | 
					b9236e09a7 | ||
| 
						 | 
					0cbc284e42 | ||
| 
						 | 
					09b38d5f42 | ||
| 
						 | 
					9e42a334fa | ||
| 
						 | 
					7bb539a06e | ||
| 
						 | 
					49438d2ee1 | ||
| 
						 | 
					5cdada8265 | ||
| 
						 | 
					735aa24020 | ||
| 
						 | 
					4147c217b1 | ||
| 
						 | 
					14f63a203d | ||
| 
						 | 
					8dda639b23 | ||
| 
						 | 
					e11c0a3633 | ||
| 
						 | 
					8487d2c9eb | ||
| 
						 | 
					ca83f139f7 | ||
| 
						 | 
					c5e583b215 | ||
| 
						 | 
					58e44b7d12 | ||
| 
						 | 
					549f618cff | ||
| 
						 | 
					8f47474edd | ||
| 
						 | 
					e9a3510346 | ||
| 
						 | 
					c5200aada8 | ||
| 
						 | 
					30e6e963b3 | ||
| 
						 | 
					4e39b93673 | ||
| 
						 | 
					c72d963f45 | ||
| 
						 | 
					f215643f2e | ||
| 
						 | 
					172d498618 | ||
| 
						 | 
					aa72be6ff3 | ||
| 
						 | 
					313993532e | ||
| 
						 | 
					b567e56b60 | ||
| 
						 | 
					e53db3582c | ||
| 
						 | 
					17431c1707 | ||
| 
						 | 
					72c6bd3f77 | ||
| 
						 | 
					7c9ebef5e9 | ||
| 
						 | 
					ca8b349df3 | ||
| 
						 | 
					72b0e11543 | ||
| 
						 | 
					1b206c3640 | ||
| 
						 | 
					9e2af9dbca | ||
| 
						 | 
					c60276fc9f | ||
| 
						 | 
					b7cc987132 | ||
| 
						 | 
					d00a3167c0 | ||
| 
						 | 
					8476c73b0c | ||
| 
						 | 
					6b1cd8c30c | ||
| 
						 | 
					e76b49afd2 | ||
| 
						 | 
					46f12dc9ad | ||
| 
						 | 
					c712b95e0d | ||
| 
						 | 
					a3e1d8ae21 | ||
| 
						 | 
					1ee1a8d6d9 | ||
| 
						 | 
					72a066b93e | ||
| 
						 | 
					f01c764589 | ||
| 
						 | 
					0327a829ac | ||
| 
						 | 
					6df66d150b | ||
| 
						 | 
					882e9b8819 | ||
| 
						 | 
					6132f94eff | ||
| 
						 | 
					ef58cfadaa | ||
| 
						 | 
					ac5e67ea73 | ||
| 
						 | 
					bf958d6113 | ||
| 
						 | 
					143d2b44d0 | ||
| 
						 | 
					71611273d7 | ||
| 
						 | 
					ef130fe377 | ||
| 
						 | 
					b27c654311 | ||
| 
						 | 
					d7d1e2100c | ||
| 
						 | 
					90930ea9f9 | ||
| 
						 | 
					af5afd7700 | ||
| 
						 | 
					1ab2185ff1 | ||
| 
						 | 
					a64df5cf0c | ||
| 
						 | 
					0f2f978d4c | ||
| 
						 | 
					3866319373 | ||
| 
						 | 
					f61963b0b0 | ||
| 
						 | 
					ec5ad809f2 | ||
| 
						 | 
					2aa413960d | ||
| 
						 | 
					268acb174d | ||
| 
						 | 
					aa4bbba5ec | ||
| 
						 | 
					3733877be1 | ||
| 
						 | 
					eba61fea2d | ||
| 
						 | 
					6d5579a8d6 | ||
| 
						 | 
					34e3455128 | ||
| 
						 | 
					306cd2f945 | ||
| 
						 | 
					07dca3e739 | ||
| 
						 | 
					5d4dd1e66f | ||
| 
						 | 
					4cb4b145f9 | ||
| 
						 | 
					5f371bdc4a | ||
| 
						 | 
					1ed417cb69 | ||
| 
						 | 
					aa58ba392e | ||
| 
						 | 
					6cf91a84ca | ||
| 
						 | 
					0485610818 | ||
| 
						 | 
					0b566980fc | ||
| 
						 | 
					7cb8becc09 | ||
| 
						 | 
					f86176b342 | ||
| 
						 | 
					8fa535a01b | ||
| 
						 | 
					c700b32670 | ||
| 
						 | 
					5f202e03a2 | ||
| 
						 | 
					22641b452a | ||
| 
						 | 
					ef2dfe330b | ||
| 
						 | 
					d3fbb8c19e | ||
| 
						 | 
					fcd86fbebd | ||
| 
						 | 
					e3bb69ff10 | ||
| 
						 | 
					960d294aa2 | ||
| 
						 | 
					770360c614 | ||
| 
						 | 
					27f8e63f3d | ||
| 
						 | 
					f302a0478f | ||
| 
						 | 
					e8d7ad58be | ||
| 
						 | 
					a88697b43a | ||
| 
						 | 
					f4d537e0f5 | ||
| 
						 | 
					cc6f140812 | ||
| 
						 | 
					d6c93dd537 | ||
| 
						 | 
					424f2b3bdc | ||
| 
						 | 
					aa5fff47de | ||
| 
						 | 
					ec0c13a600 | ||
| 
						 | 
					c920d8423d | ||
| 
						 | 
					a1f03bec4c | ||
| 
						 | 
					b8fcfe2e7c | ||
| 
						 | 
					b5bd4a5e0e | ||
| 
						 | 
					072c96e1ec | ||
| 
						 | 
					7c2e49bfdb | ||
| 
						 | 
					38c8ece642 | ||
| 
						 | 
					f80fe6d041 | ||
| 
						 | 
					9bf1576fb4 | ||
| 
						 | 
					72f80a96bc | ||
| 
						 | 
					d648e177c4 | ||
| 
						 | 
					2de655a1cf | ||
| 
						 | 
					e8f62af7f6 | ||
| 
						 | 
					da2bd4a501 | ||
| 
						 | 
					e746aafa2f | ||
| 
						 | 
					e0aa62c40d | ||
| 
						 | 
					71f7a1b166 | ||
| 
						 | 
					9d26a892d1 | ||
| 
						 | 
					678f570a90 | ||
| 
						 | 
					4ece7f2847 | ||
| 
						 | 
					4bc1f195db | ||
| 
						 | 
					32368caf1b | ||
| 
						 | 
					b2ff49ee94 | ||
| 
						 | 
					e91f54e79e | ||
| 
						 | 
					d9558f13ad | ||
| 
						 | 
					bb8f4c57c4 | ||
| 
						 | 
					8dc8f2567e | ||
| 
						 | 
					43bfac99b6 | ||
| 
						 | 
					316636f83c | ||
| 
						 | 
					be379b6d63 | ||
| 
						 | 
					e3c2fbf82a | ||
| 
						 | 
					17f3c9b840 | ||
| 
						 | 
					1b6175e598 | ||
| 
						 | 
					24de97fac2 | ||
| 
						 | 
					26dc479596 | ||
| 
						 | 
					bf27b44fee | ||
| 
						 | 
					dca25dbb78 | ||
| 
						 | 
					1802b4fe4d | ||
| 
						 | 
					c55275872e | ||
| 
						 | 
					241a5c7bc9 | ||
| 
						 | 
					d4d1fb26f6 | ||
| 
						 | 
					557d547bf1 | ||
| 
						 | 
					a48cb7bc34 | ||
| 
						 | 
					2e7b75affb | ||
| 
						 | 
					8af6e9290f | ||
| 
						 | 
					bc21a1d443 | ||
| 
						 | 
					14b277d813 | ||
| 
						 | 
					3fc9e10a24 | ||
| 
						 | 
					20915038a3 | ||
| 
						 | 
					5fa1aa2060 | ||
| 
						 | 
					15723457cb | ||
| 
						 | 
					be8a0ec184 | ||
| 
						 | 
					ec79fc933c | ||
| 
						 | 
					b02e3aad95 | ||
| 
						 | 
					3ed5a420a7 | ||
| 
						 | 
					08eca511ad | ||
| 
						 | 
					711e701933 | ||
| 
						 | 
					c34e911596 | ||
| 
						 | 
					e6bbd83c6b | ||
| 
						 | 
					8a452c3072 | ||
| 
						 | 
					b99b310306 | ||
| 
						 | 
					13bfb14107 | ||
| 
						 | 
					e3c46016e0 | ||
| 
						 | 
					4188b0969e | ||
| 
						 | 
					eb739fcccb | ||
| 
						 | 
					0c27795a10 | ||
| 
						 | 
					9a94505f43 | ||
| 
						 | 
					d05693c5c1 | ||
| 
						 | 
					7c5c3d8a3c | ||
| 
						 | 
					c0b2063b38 | ||
| 
						 | 
					7027f9a55b | ||
| 
						 | 
					4d183747b1 | ||
| 
						 | 
					cf3d8af260 | ||
| 
						 | 
					08fe1b2f75 | ||
| 
						 | 
					4ea7352674 | ||
| 
						 | 
					db3e8a267e | ||
| 
						 | 
					6e6885849b | ||
| 
						 | 
					8fc62682c4 | ||
| 
						 | 
					827f2b9739 | ||
| 
						 | 
					75031914a3 | ||
| 
						 | 
					54d70623ab | ||
| 
						 | 
					a4c9fdd95a | ||
| 
						 | 
					ed819e8c79 | ||
| 
						 | 
					6a9bfeb5aa | ||
| 
						 | 
					94680c4411 | ||
| 
						 | 
					e654766f60 | ||
| 
						 | 
					8bcbb68438 | ||
| 
						 | 
					0ef6955f96 | ||
| 
						 | 
					b9f31d42f4 | ||
| 
						 | 
					b4501557c9 | ||
| 
						 | 
					dc2b768c6b | ||
| 
						 | 
					a2ed99e6cb | ||
| 
						 | 
					60df732db7 | ||
| 
						 | 
					6bd6bb3885 | ||
| 
						 | 
					6d85d128cc | ||
| 
						 | 
					399cf65fc9 | ||
| 
						 | 
					96785ca037 | ||
| 
						 | 
					24906a6df1 | ||
| 
						 | 
					7cfc747653 | ||
| 
						 | 
					d772bbebe6 | ||
| 
						 | 
					05f911628a | ||
| 
						 | 
					14988853a3 | ||
| 
						 | 
					18b718a328 | ||
| 
						 | 
					7b3f16ac9f | ||
| 
						 | 
					6d9b9bd3f7 | ||
| 
						 | 
					82b2755c18 | ||
| 
						 | 
					04e990aeb3 | ||
| 
						 | 
					ff4b267858 | ||
| 
						 | 
					cbbcd4c571 | ||
| 
						 | 
					a590d0497f | ||
| 
						 | 
					5ad9adeb4a | ||
| 
						 | 
					ac30d906f0 | ||
| 
						 | 
					13c9594be0 | ||
| 
						 | 
					5bc071e038 | ||
| 
						 | 
					e4f5f78bfa | ||
| 
						 | 
					88b956cf98 | ||
| 
						 | 
					bfa0208692 | ||
| 
						 | 
					f725cf4661 | ||
| 
						 | 
					64994cdeda | ||
| 
						 | 
					057cc1e8a6 | ||
| 
						 | 
					9796a841d9 | ||
| 
						 | 
					de122735b8 | ||
| 
						 | 
					404b493d23 | ||
| 
						 | 
					e87ede981c | ||
| 
						 | 
					c04f9eb993 | ||
| 
						 | 
					606fb498e1 | ||
| 
						 | 
					825da42208 | ||
| 
						 | 
					a0c06e40a4 | ||
| 
						 | 
					721a8f1ecb | ||
| 
						 | 
					aba8f57279 | ||
| 
						 | 
					586a174d6c | ||
| 
						 | 
					960286a350 | ||
| 
						 | 
					bec6966542 | ||
| 
						 | 
					8c93fa51f6 | ||
| 
						 | 
					b1f4e1d4d8 | ||
| 
						 | 
					cb0e7d64ff | ||
| 
						 | 
					d7d64e8259 | ||
| 
						 | 
					8e7413da97 | ||
| 
						 | 
					08b498fccd | ||
| 
						 | 
					a36f14eb94 | ||
| 
						 | 
					ecc5a52232 | ||
| 
						 | 
					f2f9f6e488 | ||
| 
						 | 
					7327b90255 | ||
| 
						 | 
					85068b8ca2 | ||
| 
						 | 
					6ce7a9c16f | ||
| 
						 | 
					f2cfcfeefc | ||
| 
						 | 
					eb2658a91c | ||
| 
						 | 
					755273a898 | ||
| 
						 | 
					9f59a18756 | ||
| 
						 | 
					d4a24a0f1d | ||
| 
						 | 
					ef06f0da98 | ||
| 
						 | 
					92281fcbb7 | ||
| 
						 | 
					4dec0e2c69 | ||
| 
						 | 
					636db4afcc | ||
| 
						 | 
					5f36f1af11 | ||
| 
						 | 
					ba25b8755e | ||
| 
						 | 
					5a1a596098 | ||
| 
						 | 
					6399d13a49 | ||
| 
						 | 
					7a25c6e062 | ||
| 
						 | 
					06fa54fd25 | ||
| 
						 | 
					a73ed71b9d | ||
| 
						 | 
					a335b965d0 | ||
| 
						 | 
					2abbb95ea2 | ||
| 
						 | 
					725adaa7d0 | ||
| 
						 | 
					1f80ba9463 | ||
| 
						 | 
					7e7e81e974 | ||
| 
						 | 
					4327fdac12 | ||
| 
						 | 
					8cfe6bfc17 | ||
| 
						 | 
					a6f05a5874 | ||
| 
						 | 
					33de83f2ac | ||
| 
						 | 
					3fb74a1598 | ||
| 
						 | 
					3f856afec8 | ||
| 
						 | 
					2ac44cdeb6 | ||
| 
						 | 
					4e4dc4cb73 | ||
| 
						 | 
					855a953010 | ||
| 
						 | 
					02a9c422fe | ||
| 
						 | 
					3264fda06e | ||
| 
						 | 
					ca69341024 | ||
| 
						 | 
					2c7d472069 | ||
| 
						 | 
					169bf069ce | ||
| 
						 | 
					8182e6797f | ||
| 
						 | 
					1bee0ab04d | ||
| 
						 | 
					99cb48ce88 | ||
| 
						 | 
					440d91dd0e | ||
| 
						 | 
					e391bfca80 | ||
| 
						 | 
					8168e246a8 | ||
| 
						 | 
					0181ad3715 | ||
| 
						 | 
					2ef07574ae | ||
| 
						 | 
					b95dff0751 | ||
| 
						 | 
					37392f2bb2 | ||
| 
						 | 
					668d4c9c64 | ||
| 
						 | 
					a80cd3848e | ||
| 
						 | 
					66c0d1b2f7 | ||
| 
						 | 
					db6ed84451 | ||
| 
						 | 
					bef0996375 | ||
| 
						 | 
					4463cc5963 | ||
| 
						 | 
					391a1e4cf0 | ||
| 
						 | 
					d316158fe2 | ||
| 
						 | 
					38facf517a | ||
| 
						 | 
					e02a8d7586 | ||
| 
						 | 
					5c9b1e8764 | ||
| 
						 | 
					9988dff885 | ||
| 
						 | 
					dbcf14b566 | ||
| 
						 | 
					35ef5674ff | ||
| 
						 | 
					cc59f0c761 | ||
| 
						 | 
					976da45bce | ||
| 
						 | 
					f263fc067a | ||
| 
						 | 
					c83ac48bd2 | ||
| 
						 | 
					6635b88baa | ||
| 
						 | 
					3d159a833e | ||
| 
						 | 
					87ea2a611f | ||
| 
						 | 
					4b09878bdd | ||
| 
						 | 
					ea8a2135cd | ||
| 
						 | 
					b0162e6a92 | ||
| 
						 | 
					76d86395ef | ||
| 
						 | 
					8ab15e5dc4 | ||
| 
						 | 
					e268870636 | ||
| 
						 | 
					d2ac807252 | ||
| 
						 | 
					49e9f41ef2 | ||
| 
						 | 
					0af01f6f1f | ||
| 
						 | 
					68cda968a1 | ||
| 
						 | 
					013b319fab | ||
| 
						 | 
					f6826fcefc | ||
| 
						 | 
					2899ba5949 | ||
| 
						 | 
					56a91db7e7 | ||
| 
						 | 
					a558b7e104 | ||
| 
						 | 
					8f4d94f046 | ||
| 
						 | 
					7a833e2233 | ||
| 
						 | 
					9446bf5f98 | ||
| 
						 | 
					bf65746d00 | ||
| 
						 | 
					23e353b18b | ||
| 
						 | 
					f08a7862de | ||
| 
						 | 
					46daaa4ba1 | ||
| 
						 | 
					023a2c2f09 | ||
| 
						 | 
					f66d59531d | ||
| 
						 | 
					1bcd0f4c1a | ||
| 
						 | 
					b76cd1d5ae | ||
| 
						 | 
					a0f3bc8ccb | ||
| 
						 | 
					55ba0d08a5 | ||
| 
						 | 
					dea72738c1 | ||
| 
						 | 
					cc78780a8e | ||
| 
						 | 
					a1d1fe7763 | ||
| 
						 | 
					4a00809061 | ||
| 
						 | 
					a39ed9764c | ||
| 
						 | 
					cd760bbd84 | ||
| 
						 | 
					aaa5ba99aa | ||
| 
						 | 
					84bdc6be7f | ||
| 
						 | 
					2113508b6d | ||
| 
						 | 
					8dd6bf8933 | ||
| 
						 | 
					7fe4212684 | ||
| 
						 | 
					08f170d217 | ||
| 
						 | 
					8bdda64794 | ||
| 
						 | 
					20da6a1c20 | ||
| 
						 | 
					ec08c24dca | ||
| 
						 | 
					8586931630 | ||
| 
						 | 
					a992a5b3b3 | ||
| 
						 | 
					667fc79a6f | ||
| 
						 | 
					0f05970141 | ||
| 
						 | 
					ac7430dded | ||
| 
						 | 
					e5e762efcd | ||
| 
						 | 
					9c73e16560 | ||
| 
						 | 
					b3d0c1ef9c | ||
| 
						 | 
					c605e222a7 | ||
| 
						 | 
					397078f7ff | ||
| 
						 | 
					578f26a738 | ||
| 
						 | 
					3ad8065e20 | ||
| 
						 | 
					24218e7570 | ||
| 
						 | 
					66c7717f04 | ||
| 
						 | 
					16f946550e | ||
| 
						 | 
					412f8ecc6c | ||
| 
						 | 
					9960a31485 | ||
| 
						 | 
					51dcf642b3 | ||
| 
						 | 
					437632f859 | ||
| 
						 | 
					bfeea555b2 | ||
| 
						 | 
					98f0f442eb | ||
| 
						 | 
					479f94c372 | ||
| 
						 | 
					3b999dc5e2 | ||
| 
						 | 
					0140713e86 | ||
| 
						 | 
					20943ad3d3 | ||
| 
						 | 
					15b2ec9721 | ||
| 
						 | 
					3b1544b5e4 | ||
| 
						 | 
					c9cd082855 | ||
| 
						 | 
					b317d597ac | ||
| 
						 | 
					d7c002890c | ||
| 
						 | 
					3bb19811e7 | ||
| 
						 | 
					348dd22279 | ||
| 
						 | 
					065a01de48 | ||
| 
						 | 
					3e99b4cbf6 | ||
| 
						 | 
					09fb2f6557 | ||
| 
						 | 
					6968da3ac7 | ||
| 
						 | 
					9ca97bf4bc | ||
| 
						 | 
					bf1c1b84c3 | ||
| 
						 | 
					0c59503edb | ||
| 
						 | 
					c70314d930 | ||
| 
						 | 
					6fa08861f8 | ||
| 
						 | 
					9104ca8e49 | ||
| 
						 | 
					72b95151a1 | ||
| 
						 | 
					2af33b3630 | ||
| 
						 | 
					378e6ec9af | ||
| 
						 | 
					654e795545 | ||
| 
						 | 
					27b68c1174 | ||
| 
						 | 
					c62ba2451e | ||
| 
						 | 
					1ae8eabe99 | ||
| 
						 | 
					d72d1b8a99 | ||
| 
						 | 
					72726c1cf9 | ||
| 
						 | 
					b939d6016b | ||
| 
						 | 
					6203d9a150 | ||
| 
						 | 
					36a2626ccc | ||
| 
						 | 
					861f11af66 | ||
| 
						 | 
					bd057a4cc9 | ||
| 
						 | 
					39fdfae541 | ||
| 
						 | 
					dc24a8c781 | ||
| 
						 | 
					9f57fb1421 | ||
| 
						 | 
					59fa21779b | ||
| 
						 | 
					059f57db2d | ||
| 
						 | 
					a140671aad | ||
| 
						 | 
					13c8be8d06 | ||
| 
						 | 
					5fe8990fb4 | ||
| 
						 | 
					e0dbfb0488 | ||
| 
						 | 
					12799b7159 | ||
| 
						 | 
					722ed128a1 | ||
| 
						 | 
					9929746b1d | ||
| 
						 | 
					cc628821e6 | ||
| 
						 | 
					d70035ff0c | ||
| 
						 | 
					857af61af1 | ||
| 
						 | 
					eec90274d8 | ||
| 
						 | 
					f6fc484cb3 | ||
| 
						 | 
					e8fff55c42 | ||
| 
						 | 
					ba07946f1e | ||
| 
						 | 
					3cf3cdd705 | ||
| 
						 | 
					600f668c63 | ||
| 
						 | 
					9801fce659 | ||
| 
						 | 
					8a8990b12b | ||
| 
						 | 
					4c1f51110b | ||
| 
						 | 
					fe85c1bbb9 | ||
| 
						 | 
					913d538587 | ||
| 
						 | 
					a89ff72308 | ||
| 
						 | 
					9e704365fc | ||
| 
						 | 
					a99e58184b | ||
| 
						 | 
					485bdbc56a | ||
| 
						 | 
					d981bc8fd0 | ||
| 
						 | 
					7000168fd4 | ||
| 
						 | 
					ff22480d89 | ||
| 
						 | 
					5694f97a6b | ||
| 
						 | 
					8ec85e2829 | ||
| 
						 | 
					b677d3fac7 | ||
| 
						 | 
					706aacd8e6 | ||
| 
						 | 
					dc6719cf54 | ||
| 
						 | 
					803bc2d9fc | ||
| 
						 | 
					7de5b55091 | ||
| 
						 | 
					7cc67cf8f0 | ||
| 
						 | 
					76c5101092 | ||
| 
						 | 
					9909c3a0ed | ||
| 
						 | 
					2f8d2f4854 | ||
| 
						 | 
					60a3751839 | ||
| 
						 | 
					b1ee34ba0c | ||
| 
						 | 
					ce87e0c40b | ||
| 
						 | 
					069ad6a09a | ||
| 
						 | 
					71cd548c00 | ||
| 
						 | 
					bf1403c818 | ||
| 
						 | 
					1ddd05302b | ||
| 
						 | 
					bcc622a24d | ||
| 
						 | 
					ea3301c75c | ||
| 
						 | 
					a06a81a415 | ||
| 
						 | 
					4b1c4f7ccc | ||
| 
						 | 
					d1950acd01 | ||
| 
						 | 
					21f2622a4b | ||
| 
						 | 
					039b70eed2 | ||
| 
						 | 
					79f6a43019 | ||
| 
						 | 
					d8e4308b1b | ||
| 
						 | 
					3d75093b2c | ||
| 
						 | 
					434fbb3463 | ||
| 
						 | 
					023dc89c3e | ||
| 
						 | 
					de3eb8969c | ||
| 
						 | 
					b5641be30d | ||
| 
						 | 
					fbd6eac877 | ||
| 
						 | 
					ff4515bbf1 | ||
| 
						 | 
					1fecab177b | ||
| 
						 | 
					5dc0fe05af | ||
| 
						 | 
					b1b385c455 | ||
| 
						 | 
					25bba912f6 | ||
| 
						 | 
					3c6e86d04b | ||
| 
						 | 
					cb6e323596 | ||
| 
						 | 
					3d2035d08a | ||
| 
						 | 
					cf4b04e047 | ||
| 
						 | 
					da86f916d8 | ||
| 
						 | 
					243b5be31c | ||
| 
						 | 
					e7a07f7e92 | ||
| 
						 | 
					c3d753cf38 | ||
| 
						 | 
					b01e6387fc | ||
| 
						 | 
					70c46d098b | ||
| 
						 | 
					d86aca0f5d | ||
| 
						 | 
					816e4a096e | ||
| 
						 | 
					09414fe36a | ||
| 
						 | 
					1d1e819817 | ||
| 
						 | 
					df0e7508db | ||
| 
						 | 
					342daf1be8 | ||
| 
						 | 
					92b1f01118 | ||
| 
						 | 
					f6bb9b582a | ||
| 
						 | 
					8fb8bd932b | ||
| 
						 | 
					aad62bc976 | ||
| 
						 | 
					3f74b94784 | ||
| 
						 | 
					38a49a7f37 | ||
| 
						 | 
					e9467341fa | ||
| 
						 | 
					f603bf6be7 | ||
| 
						 | 
					131e051ddc | ||
| 
						 | 
					6cc05e9e27 | ||
| 
						 | 
					f626fe3166 | ||
| 
						 | 
					c00e67542f | ||
| 
						 | 
					6bc57b6132 | ||
| 
						 | 
					22660362a3 | ||
| 
						 | 
					d972e97c88 | ||
| 
						 | 
					b1ab9975b7 | ||
| 
						 | 
					3991f4daec | ||
| 
						 | 
					7ca89d8b54 | ||
| 
						 | 
					f6b567d6fc | ||
| 
						 | 
					998b07ab8c | ||
| 
						 | 
					8addba8203 | ||
| 
						 | 
					4936896ff7 | ||
| 
						 | 
					3ab930a107 | ||
| 
						 | 
					18b7484c5b | ||
| 
						 | 
					de512a5ea2 | ||
| 
						 | 
					6754c8e85e | ||
| 
						 | 
					113cfae2dc | ||
| 
						 | 
					57243ff010 | ||
| 
						 | 
					33aebf9cb5 | ||
| 
						 | 
					7c4dfe96ee | ||
| 
						 | 
					6e58ddf681 | ||
| 
						 | 
					bf19120c27 | ||
| 
						 | 
					cae5c049e4 | ||
| 
						 | 
					3d37087916 | ||
| 
						 | 
					ff76e4bd89 | ||
| 
						 | 
					6949f1328c | ||
| 
						 | 
					a0a506a3c4 | ||
| 
						 | 
					b63090c20f | ||
| 
						 | 
					aa5a4a9977 | ||
| 
						 | 
					b15ebda948 | ||
| 
						 | 
					abf4f061c1 | ||
| 
						 | 
					252eef2e5e | ||
| 
						 | 
					245cd3ee1a | ||
| 
						 | 
					3bea8f9706 | ||
| 
						 | 
					45cb29d9a0 | ||
| 
						 | 
					dfe808bee7 | ||
| 
						 | 
					d974b1ff0e | ||
| 
						 | 
					58f3dd5336 | ||
| 
						 | 
					56269170cb | ||
| 
						 | 
					b00f16648f | ||
| 
						 | 
					4290c4ca22 | ||
| 
						 | 
					fc66c31e89 | ||
| 
						 | 
					7f7c8e831e | ||
| 
						 | 
					a26c849532 | ||
| 
						 | 
					8f057ca9d1 | ||
| 
						 | 
					d2991e60b6 | ||
| 
						 | 
					4a56621ec3 | ||
| 
						 | 
					10ba1430f9 | ||
| 
						 | 
					a398e7a550 | ||
| 
						 | 
					6d71f24f75 | ||
| 
						 | 
					96816c12ca | ||
| 
						 | 
					8f4d20e411 | ||
| 
						 | 
					9984926f69 | ||
| 
						 | 
					cf758d773e | ||
| 
						 | 
					a2a6081027 | ||
| 
						 | 
					c012f0c4c5 | ||
| 
						 | 
					5a10ed37a7 | ||
| 
						 | 
					9e85900057 | ||
| 
						 | 
					1a9dd9de0b | ||
| 
						 | 
					68e3b787f9 | ||
| 
						 | 
					0dae5bef71 | ||
| 
						 | 
					6706704ad6 | ||
| 
						 | 
					b4413ed726 | ||
| 
						 | 
					1067fa015e | ||
| 
						 | 
					5e1fe88b8b | ||
| 
						 | 
					411335ebcb | ||
| 
						 | 
					91ed41b536 | ||
| 
						 | 
					f19c15491e | ||
| 
						 | 
					024c0032eb | ||
| 
						 | 
					0da81a4d10 | ||
| 
						 | 
					4a9f7e3bce | ||
| 
						 | 
					049839a5c7 | ||
| 
						 | 
					cf4dcc34ec | ||
| 
						 | 
					a901927844 | ||
| 
						 | 
					4d612c15af | ||
| 
						 | 
					07ab0bc5a1 | ||
| 
						 | 
					8aec87cc02 | ||
| 
						 | 
					a6025e6fab | ||
| 
						 | 
					442e411cde | ||
| 
						 | 
					e841a61bf0 | ||
| 
						 | 
					acec0194de | ||
| 
						 | 
					2f1c2110c6 | ||
| 
						 | 
					8557f5b94a | ||
| 
						 | 
					8e573eea4c | ||
| 
						 | 
					babef8baae | ||
| 
						 | 
					149765764e | ||
| 
						 | 
					efd4ab46f5 | ||
| 
						 | 
					45d6444c7e | ||
| 
						 | 
					ae8239e5de | ||
| 
						 | 
					aa65f49190 | ||
| 
						 | 
					f0994ba457 | ||
| 
						 | 
					1bda41ff67 | ||
| 
						 | 
					dae91ed243 | ||
| 
						 | 
					5a799139cd | ||
| 
						 | 
					de42a428e6 | ||
| 
						 | 
					eb36d0742a | ||
| 
						 | 
					63c7041e1f | ||
| 
						 | 
					d8a9123852 | ||
| 
						 | 
					b1263ddc69 | ||
| 
						 | 
					19031c2197 | ||
| 
						 | 
					7e50e17aaf | ||
| 
						 | 
					7a2ffdf39c | ||
| 
						 | 
					a7265c4251 | ||
| 
						 | 
					ce8fa79206 | ||
| 
						 | 
					6f39f639bd | ||
| 
						 | 
					b05c63549d | ||
| 
						 | 
					a7db123437 | ||
| 
						 | 
					9f98660423 | ||
| 
						 | 
					241c714a8b | ||
| 
						 | 
					7b0fa862d2 | ||
| 
						 | 
					67ac3cfe32 | ||
| 
						 | 
					bfe2d4d573 | ||
| 
						 | 
					c926e0afcc | ||
| 
						 | 
					26950c5673 | ||
| 
						 | 
					5bc07e6d57 | ||
| 
						 | 
					86788362a5 | ||
| 
						 | 
					c3666a9a71 | ||
| 
						 | 
					deedc5fb2e | ||
| 
						 | 
					23b5ffa97d | ||
| 
						 | 
					f4fbe67db9 | ||
| 
						 | 
					a2c7a75705 | ||
| 
						 | 
					599ce0eade | ||
| 
						 | 
					d68f2ef12c | ||
| 
						 | 
					11f3ab8dc7 | ||
| 
						 | 
					67d30353f0 | ||
| 
						 | 
					75b4a6dd46 | ||
| 
						 | 
					4813163eac | ||
| 
						 | 
					86bbea337d | ||
| 
						 | 
					5c5210625e | ||
| 
						 | 
					e63a30064b | ||
| 
						 | 
					a4a1eec30b | ||
| 
						 | 
					222b1ddbd9 | ||
| 
						 | 
					d35164506a | ||
| 
						 | 
					a88dd88a07 | ||
| 
						 | 
					1ed08f01ea | ||
| 
						 | 
					a47c01fd41 | ||
| 
						 | 
					eca07ab830 | ||
| 
						 | 
					35e385f7a7 | ||
| 
						 | 
					3512715704 | ||
| 
						 | 
					191e3b7d2c | ||
| 
						 | 
					6d07881141 | ||
| 
						 | 
					13fbbc190a | ||
| 
						 | 
					251fe626f2 | ||
| 
						 | 
					61c6a6e7f3 | ||
| 
						 | 
					5fee3a9288 | ||
| 
						 | 
					911f218f0b | ||
| 
						 | 
					9b68d8101e | ||
| 
						 | 
					af41350eb9 | ||
| 
						 | 
					cfe6f27d48 | ||
| 
						 | 
					8f63e348b3 | ||
| 
						 | 
					b314dd0900 | ||
| 
						 | 
					8901cf8e81 | ||
| 
						 | 
					950fab6374 | ||
| 
						 | 
					e11736e1d2 | ||
| 
						 | 
					9d1f5c42ce | ||
| 
						 | 
					c062fba69c | ||
| 
						 | 
					a84046390b | ||
| 
						 | 
					4628095fe7 | ||
| 
						 | 
					aa29323a8a | ||
| 
						 | 
					787ed9bc0f | ||
| 
						 | 
					d5617b7c3a | ||
| 
						 | 
					daaff8974e | ||
| 
						 | 
					1ef60a9e5e | ||
| 
						 | 
					4fcbd511c0 | ||
| 
						 | 
					fb6e395ad8 | ||
| 
						 | 
					64d6bff6d9 | ||
| 
						 | 
					d9216060bc | ||
| 
						 | 
					998cbace92 | ||
| 
						 | 
					bcaa9a92e5 | ||
| 
						 | 
					2204038951 | ||
| 
						 | 
					576adc9036 | ||
| 
						 | 
					998f67ac5d | ||
| 
						 | 
					00de18be9a | ||
| 
						 | 
					be981a2c9b | ||
| 
						 | 
					c61d32816a | ||
| 
						 | 
					da3cf0d4a3 | ||
| 
						 | 
					f3fbb0b89c | ||
| 
						 | 
					7124b9a36c | ||
| 
						 | 
					e311a39632 | ||
| 
						 | 
					712a6b6c39 | ||
| 
						 | 
					51407abe44 | ||
| 
						 | 
					ed0a1c57d6 | ||
| 
						 | 
					8a470b1038 | ||
| 
						 | 
					7858228505 | ||
| 
						 | 
					baddabaa16 | ||
| 
						 | 
					2525a22d78 | ||
| 
						 | 
					427b434ce3 | ||
| 
						 | 
					7b4ef8fc31 | ||
| 
						 | 
					5f921965e6 | ||
| 
						 | 
					ce14cd02e4 | ||
| 
						 | 
					1e705c8ed5 | ||
| 
						 | 
					05f501af52 | ||
| 
						 | 
					b8ae65bb30 | ||
| 
						 | 
					e94fa4844f | ||
| 
						 | 
					321e2087ea | ||
| 
						 | 
					e0965aae5e | ||
| 
						 | 
					aac60edce2 | ||
| 
						 | 
					414c1de963 | ||
| 
						 | 
					9dc9a6923e | ||
| 
						 | 
					b589102be8 | ||
| 
						 | 
					7ca4dfe09b | ||
| 
						 | 
					df367e0d47 | ||
| 
						 | 
					c584b82ddb | ||
| 
						 | 
					680219ebcb | ||
| 
						 | 
					5f17ab2501 | ||
| 
						 | 
					ef87487f60 | ||
| 
						 | 
					c84e912dd8 | ||
| 
						 | 
					8bafe72434 | ||
| 
						 | 
					2ebff2623f | ||
| 
						 | 
					76dcc69e44 | ||
| 
						 | 
					72418ce4d7 | ||
| 
						 | 
					682ef22194 | ||
| 
						 | 
					e221b1eed4 | ||
| 
						 | 
					ee8dd41605 | ||
| 
						 | 
					696306f066 | ||
| 
						 | 
					d0b8d666e4 | ||
| 
						 | 
					1807d5b5d4 | ||
| 
						 | 
					4a81826d19 | ||
| 
						 | 
					85c12aa322 | ||
| 
						 | 
					848a10c3a8 | ||
| 
						 | 
					da9d0dc3bc | ||
| 
						 | 
					a4baa29678 | ||
| 
						 | 
					daaca822ac | ||
| 
						 | 
					a9b5280691 | ||
| 
						 | 
					59ced3f947 | ||
| 
						 | 
					264706210b | ||
| 
						 | 
					22ae7dd1f3 | ||
| 
						 | 
					7677ae254f | ||
| 
						 | 
					1816e9d5cf | ||
| 
						 | 
					7ccb4c5f06 | ||
| 
						 | 
					9be6755f65 | ||
| 
						 | 
					01205af018 | ||
| 
						 | 
					e5fb986463 | ||
| 
						 | 
					5b5150e6d4 | ||
| 
						 | 
					55d24e577e | ||
| 
						 | 
					9d8e3f5049 | ||
| 
						 | 
					7f50fa3fcf | ||
| 
						 | 
					99ede83bdc | ||
| 
						 | 
					fff8b78aba | ||
| 
						 | 
					61488e840d | ||
| 
						 | 
					7ca1989d98 | ||
| 
						 | 
					a80d01209c | ||
| 
						 | 
					4595dcb7ed | ||
| 
						 | 
					d2a8d655c8 | ||
| 
						 | 
					a688d3feb5 | ||
| 
						 | 
					80e2b34abc | ||
| 
						 | 
					7d1d88a32f | ||
| 
						 | 
					fb3d43d2d5 | ||
| 
						 | 
					d95c048edd | ||
| 
						 | 
					ebdec4fa44 | ||
| 
						 | 
					df2fc9d77c | ||
| 
						 | 
					ead8dbbaa5 | ||
| 
						 | 
					d7e815d2bb | ||
| 
						 | 
					95615a5501 | ||
| 
						 | 
					f58b0a65f0 | ||
| 
						 | 
					75487b1f58 | ||
| 
						 | 
					b59ad521ca | ||
| 
						 | 
					c7daaa00cd | ||
| 
						 | 
					b47ff975b0 | ||
| 
						 | 
					606f24e981 | ||
| 
						 | 
					d043a87b30 | ||
| 
						 | 
					c83d9287a7 | ||
| 
						 | 
					4cae7525d9 | ||
| 
						 | 
					fa8092b2cd | ||
| 
						 | 
					76966d2ce7 | ||
| 
						 | 
					b2b07a2be6 | ||
| 
						 | 
					5a740aecb0 | ||
| 
						 | 
					a21628e350 | ||
| 
						 | 
					1ae79331e7 | ||
| 
						 | 
					9fcd686fda | ||
| 
						 | 
					8b14e141d0 | ||
| 
						 | 
					1759fd4cf9 | ||
| 
						 | 
					9cbc6c91c4 | ||
| 
						 | 
					12e7837602 | ||
| 
						 | 
					21c3a419a5 | ||
| 
						 | 
					ff349f2edf | ||
| 
						 | 
					287fac3a89 | ||
| 
						 | 
					088d8ca207 | ||
| 
						 | 
					ba206bb387 | ||
| 
						 | 
					c37902d3a4 | ||
| 
						 | 
					4fc01f3f7b | ||
| 
						 | 
					da38684cdd | ||
| 
						 | 
					f5ed71bcc6 | ||
| 
						 | 
					cb2cb72333 | ||
| 
						 | 
					8fc26183e9 | ||
| 
						 | 
					d53c8f9830 | ||
| 
						 | 
					e8ae8fddb7 | ||
| 
						 | 
					c8136d4231 | ||
| 
						 | 
					b876867297 | ||
| 
						 | 
					e4b8380530 | ||
| 
						 | 
					91dfd59731 | ||
| 
						 | 
					fbd599194c | ||
| 
						 | 
					5fdff90a10 | ||
| 
						 | 
					0270ec26fb | ||
| 
						 | 
					96c62619e6 | ||
| 
						 | 
					79e342be57 | ||
| 
						 | 
					083155413d | ||
| 
						 | 
					c89f23b018 | ||
| 
						 | 
					d83019cbe4 | ||
| 
						 | 
					f971ec5d34 | ||
| 
						 | 
					cc7271aa73 | ||
| 
						 | 
					c5776ce41f | ||
| 
						 | 
					f873d6b375 | ||
| 
						 | 
					75c5ebbffa | ||
| 
						 | 
					c86169022a | ||
| 
						 | 
					d51a724ade | ||
| 
						 | 
					db0a79da93 | ||
| 
						 | 
					c1143d7a6d | ||
| 
						 | 
					48393e0e83 | ||
| 
						 | 
					f3697431a4 | ||
| 
						 | 
					7b4730271d | ||
| 
						 | 
					e3d6e5f420 | ||
| 
						 | 
					9cbe36d4c6 | ||
| 
						 | 
					af6f7a7146 | ||
| 
						 | 
					b25bb2cc53 | ||
| 
						 | 
					c18d95e89a | ||
| 
						 | 
					79ded6018b | ||
| 
						 | 
					fa42d03a65 | ||
| 
						 | 
					59f316b341 | ||
| 
						 | 
					22f7de2c09 | ||
| 
						 | 
					f307b8ba7a | ||
| 
						 | 
					b4b9df81cb | ||
| 
						 | 
					5034a20345 | ||
| 
						 | 
					2a71c2b0e7 | ||
| 
						 | 
					26944f9e39 | ||
| 
						 | 
					c37b3d3df5 | ||
| 
						 | 
					e64946c3b6 | ||
| 
						 | 
					ae135b89d6 | ||
| 
						 | 
					e0a62d9b35 | ||
| 
						 | 
					4112426848 | ||
| 
						 | 
					39dbffd8d0 | ||
| 
						 | 
					638b399b31 | ||
| 
						 | 
					952d6183ed | ||
| 
						 | 
					8a5713ef7c | ||
| 
						 | 
					3365a6008d | ||
| 
						 | 
					382d7bfaa1 | ||
| 
						 | 
					2e13ddf405 | ||
| 
						 | 
					3daf2f7738 | ||
| 
						 | 
					1d3acc8ed3 | ||
| 
						 | 
					944d35971d | ||
| 
						 | 
					fa341bab30 | ||
| 
						 | 
					9ce8af0b82 | ||
| 
						 | 
					036a6e3e41 | ||
| 
						 | 
					d825b9ec26 | ||
| 
						 | 
					f4c6ca4554 | ||
| 
						 | 
					9afe4aea4b | ||
| 
						 | 
					327929243c | ||
| 
						 | 
					3cc8c3284a | ||
| 
						 | 
					f4349c7a8c | ||
| 
						 | 
					e2c18c4e1e | ||
| 
						 | 
					4b46d847f0 | ||
| 
						 | 
					81fe768f6a | ||
| 
						 | 
					c3f016eae8 | ||
| 
						 | 
					8923779ab0 | ||
| 
						 | 
					ebd3ef842f | ||
| 
						 | 
					583218a045 | ||
| 
						 | 
					18c033d57f | ||
| 
						 | 
					153b2bfa53 | ||
| 
						 | 
					b676f80110 | ||
| 
						 | 
					0a9b325360 | ||
| 
						 | 
					f7fbaa534d | ||
| 
						 | 
					af9e6f2c46 | ||
| 
						 | 
					ea93a22e14 | ||
| 
						 | 
					0e31726fd3 | ||
| 
						 | 
					9f7e6778c5 | ||
| 
						 | 
					590d9c2e61 | ||
| 
						 | 
					6c31a2bfa6 | ||
| 
						 | 
					c12fe9f25e | ||
| 
						 | 
					f943669e18 | ||
| 
						 | 
					9bd83f3acf | ||
| 
						 | 
					3b26735998 | ||
| 
						 | 
					aada15fc41 | ||
| 
						 | 
					79d25769ee | ||
| 
						 | 
					6a2d383be6 | ||
| 
						 | 
					1dd6800987 | ||
| 
						 | 
					545c8fa482 | ||
| 
						 | 
					5e673a9ee0 | ||
| 
						 | 
					e3d18643b8 | ||
| 
						 | 
					92eb67a2af | ||
| 
						 | 
					2f318bbe9f | ||
| 
						 | 
					b1bed59be2 | ||
| 
						 | 
					fac7eb61b3 | ||
| 
						 | 
					0ac732a3a3 | ||
| 
						 | 
					4450d03a54 | ||
| 
						 | 
					bf3f68fa19 | ||
| 
						 | 
					e4e7868b30 | ||
| 
						 | 
					46a551df16 | ||
| 
						 | 
					3d7fa680cb | ||
| 
						 | 
					20a12462b1 | ||
| 
						 | 
					c38035e25e | ||
| 
						 | 
					a49fb1940e | ||
| 
						 | 
					8d4fdaf902 | ||
| 
						 | 
					32774d23c7 | ||
| 
						 | 
					2820adad53 | ||
| 
						 | 
					7ecd7eeba1 | ||
| 
						 | 
					f7a427d2c0 | ||
| 
						 | 
					0cc9cf8b45 | ||
| 
						 | 
					59ed8c9660 | ||
| 
						 | 
					d06f94bddd | ||
| 
						 | 
					71562ab0e5 | ||
| 
						 | 
					b5955f08c9 | ||
| 
						 | 
					278ddf037f | ||
| 
						 | 
					c120569894 | ||
| 
						 | 
					ac3151af92 | ||
| 
						 | 
					aa376f1737 | ||
| 
						 | 
					1189ad7862 | ||
| 
						 | 
					88c13e7b9a | ||
| 
						 | 
					596baac62e | ||
| 
						 | 
					5a8503ec14 | ||
| 
						 | 
					e44f95724d | ||
| 
						 | 
					dec0d0dea7 | ||
| 
						 | 
					46f96e94ec | ||
| 
						 | 
					89b30bcf58 | ||
| 
						 | 
					473d8f5f85 | ||
| 
						 | 
					08d8d65599 | ||
| 
						 | 
					2964c1d82e | ||
| 
						 | 
					75ab8552d9 | ||
| 
						 | 
					b78d5b3d03 | ||
| 
						 | 
					064081c771 | ||
| 
						 | 
					3525d35d15 | ||
| 
						 | 
					9d9ee7c585 | ||
| 
						 | 
					e65c32c4c7 | ||
| 
						 | 
					113769791f | ||
| 
						 | 
					5d2a1d21d5 | ||
| 
						 | 
					6e40f92aaf | ||
| 
						 | 
					302cb8a5be | ||
| 
						 | 
					0b27890484 | ||
| 
						 | 
					5db247e632 | ||
| 
						 | 
					c1155a4338 | ||
| 
						 | 
					da806b9492 | ||
| 
						 | 
					3d6d46b30d | ||
| 
						 | 
					57c69738ba | ||
| 
						 | 
					0c8157dbc0 | ||
| 
						 | 
					d7b278f2f7 | ||
| 
						 | 
					18d74f1057 | ||
| 
						 | 
					e04856f794 | ||
| 
						 | 
					d7bbfb0fc3 | ||
| 
						 | 
					a0857817de | ||
| 
						 | 
					8d39234fd0 | ||
| 
						 | 
					0fce1c0fd8 | ||
| 
						 | 
					c23c285f82 | ||
| 
						 | 
					81b560d791 | ||
| 
						 | 
					55459a4f42 | ||
| 
						 | 
					9eafd3e6ca | ||
| 
						 | 
					e7ac26ff5a | ||
| 
						 | 
					fad78641d6 | ||
| 
						 | 
					a32cb0142b | ||
| 
						 | 
					4a3c133152 | ||
| 
						 | 
					ae79c34508 | ||
| 
						 | 
					7eb69c1ad7 | ||
| 
						 | 
					8aaff0af89 | ||
| 
						 | 
					4b77280c65 | ||
| 
						 | 
					a0df84e4f2 | ||
| 
						 | 
					8876e6a098 | ||
| 
						 | 
					9bee2a90f9 | ||
| 
						 | 
					cd6a811bbd | ||
| 
						 | 
					84f9a83f55 | ||
| 
						 | 
					2efc669ab2 | ||
| 
						 | 
					5444ed77ad | ||
| 
						 | 
					dd71fe80a5 | ||
| 
						 | 
					b73092eb64 | ||
| 
						 | 
					aa28f87b0c | ||
| 
						 | 
					1e1bcd4a30 | ||
| 
						 | 
					d24b3c46bf | ||
| 
						 | 
					37222f07d9 | ||
| 
						 | 
					49646a79e5 | ||
| 
						 | 
					9940e1210e | ||
| 
						 | 
					10892812c3 | ||
| 
						 | 
					d8ff5987dd | ||
| 
						 | 
					d014d418e9 | ||
| 
						 | 
					cc1b56501d | ||
| 
						 | 
					a5ad9648bf | ||
| 
						 | 
					3630099234 | ||
| 
						 | 
					941a24c75b | ||
| 
						 | 
					50bdc01484 | ||
| 
						 | 
					d9e340ddc4 | ||
| 
						 | 
					beee1e91d6 | ||
| 
						 | 
					35935d2bac | ||
| 
						 | 
					5d0b0ad33e | ||
| 
						 | 
					d1a6ac531f | ||
| 
						 | 
					ae255a3bd9 | ||
| 
						 | 
					fa733afa51 | ||
| 
						 | 
					b60c723457 | ||
| 
						 | 
					2bb7ff3c13 | ||
| 
						 | 
					68b4cf00e2 | ||
| 
						 | 
					440169ff60 | ||
| 
						 | 
					1fc361242e | ||
| 
						 | 
					72cc6f3d75 | ||
| 
						 | 
					a2b1924e00 | ||
| 
						 | 
					7e926167f0 | ||
| 
						 | 
					32484bd32e | ||
| 
						 | 
					04ade1008b | ||
| 
						 | 
					4603de0e70 | ||
| 
						 | 
					12c1ff65e9 | ||
| 
						 | 
					7b8cbf7f86 | ||
| 
						 | 
					c198e83f70 | ||
| 
						 | 
					e60863a290 | ||
| 
						 | 
					946563d3b2 | ||
| 
						 | 
					3f2ef1d54e | ||
| 
						 | 
					0d2b60b905 | ||
| 
						 | 
					161b11e428 | ||
| 
						 | 
					f7748d51df | ||
| 
						 | 
					5d13b4b705 | ||
| 
						 | 
					92e52a2284 | ||
| 
						 | 
					3efd5fb77a | ||
| 
						 | 
					c3d62bb8d8 | ||
| 
						 | 
					c099e843d5 | ||
| 
						 | 
					5f3a5871f1 | ||
| 
						 | 
					f01fdd0070 | ||
| 
						 | 
					d20cc367b8 | ||
| 
						 | 
					eadb9a733f | ||
| 
						 | 
					32ac454b5b | ||
| 
						 | 
					2cadd6af44 | ||
| 
						 | 
					3c8b5cb313 | ||
| 
						 | 
					fcf2387794 | ||
| 
						 | 
					f9783b4806 | ||
| 
						 | 
					cbdc532f83 | ||
| 
						 | 
					124554bae5 | ||
| 
						 | 
					2725536d7d | ||
| 
						 | 
					beeef7a4f7 | ||
| 
						 | 
					7f2ebb6aeb | ||
| 
						 | 
					c8e30383ba | ||
| 
						 | 
					a7d5a6ccb9 | ||
| 
						 | 
					be3380aaf3 | ||
| 
						 | 
					40a4ab5410 | ||
| 
						 | 
					54a2181960 | ||
| 
						 | 
					e490e15bc4 | ||
| 
						 | 
					469844d97b | ||
| 
						 | 
					892ea29ba8 | ||
| 
						 | 
					acde1c6742 | ||
| 
						 | 
					07da11d852 | ||
| 
						 | 
					502b8c2270 | ||
| 
						 | 
					af3f7ac810 | ||
| 
						 | 
					935c6caf96 | ||
| 
						 | 
					90bce1d437 | ||
| 
						 | 
					831dd3e2e0 | ||
| 
						 | 
					b8e208eb32 | ||
| 
						 | 
					542c908e30 | ||
| 
						 | 
					08529242bf | ||
| 
						 | 
					1ae9449d92 | ||
| 
						 | 
					7fd0b1fa08 | ||
| 
						 | 
					111572e3f2 | ||
| 
						 | 
					c9875d24b4 | ||
| 
						 | 
					94a75603c3 | ||
| 
						 | 
					d55925bccd | ||
| 
						 | 
					0b5adcbfab | ||
| 
						 | 
					8d84562f32 | ||
| 
						 | 
					136d159833 | ||
| 
						 | 
					e3026062d8 | ||
| 
						 | 
					8a69c69b7f | ||
| 
						 | 
					197714a57a | ||
| 
						 | 
					567cdc6f8d | ||
| 
						 | 
					be6e8c7713 | ||
| 
						 | 
					f667b35403 | ||
| 
						 | 
					942e482ca4 | ||
| 
						 | 
					10b381965e | ||
| 
						 | 
					79c3a99a38 | ||
| 
						 | 
					463d3c8e97 | ||
| 
						 | 
					da007729c6 | ||
| 
						 | 
					b7b42b5fd4 | ||
| 
						 | 
					735f9bd053 | ||
| 
						 | 
					6096b65374 | ||
| 
						 | 
					727d342eb3 | ||
| 
						 | 
					58815ba8bc | ||
| 
						 | 
					49d0502775 | ||
| 
						 | 
					0b6c6e6d2c | ||
| 
						 | 
					45ede6047a | ||
| 
						 | 
					8f469d4ebb | ||
| 
						 | 
					f3264d056d | 
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										449
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										449
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,5 +1,444 @@
 | 
			
		||||
# 更新日志
 | 
			
		||||
 | 
			
		||||
## v4.1.9
 | 
			
		||||
 | 
			
		||||
- 功能优化:优化系统配置,移除已废弃的配置项
 | 
			
		||||
- 功能优化:GPT-O1 模型支持流式输出
 | 
			
		||||
- 功能优化:优化代码引用快样式,支持主题切换
 | 
			
		||||
- 功能优化:登录,注册页面允许替换用户自己的 Logo 和 Title
 | 
			
		||||
- Bug 修复:修复 OpenAI 实时语音通话没有检测用户算力不足的 Bug
 | 
			
		||||
- 功能新增:管理后台增加算力日志查询功能,支持按用户,按模型,按日期,按类型查询算力日志
 | 
			
		||||
- 功能优化:支持为模型绑定 Dalle 和 chat 类型的 API KEY
 | 
			
		||||
- 功能新增:支持管理后台设置 ICP 备案号
 | 
			
		||||
 | 
			
		||||
## v4.1.8
 | 
			
		||||
 | 
			
		||||
- 功能优化:**UI 全新改版,支持主题切换**。 :rocket: :rocket: :rocket:
 | 
			
		||||
- 功能新增:Gitee AI API 接口接入,目前支持 Gitee 的 SD 绘图接口,支持 Gitee 的 AI 对话接口。:rocket: :rocket: :rocket:
 | 
			
		||||
- Bug 修复:修复音 Luma API 更新导致任务响应解析失败的错误
 | 
			
		||||
- 功能优化:支持 Suno v4.0 模型支持
 | 
			
		||||
- Bug 修复:修复 Suno 已完成任务删除失败的 错误
 | 
			
		||||
- 功能新增:支持 OpenAI 实时语音通话功能,目前已经支持按次收费,支持管理员设置每次实时语音通话的算力消耗
 | 
			
		||||
- 功能新增:生成提示词需要消耗算力,支持管理员设置每次生成提示词的算力消耗,防止被白嫖
 | 
			
		||||
- 功能新增:DALL-E-3 绘图支持 Flux 绘图模型,支持在管理后添加 Flux,SD 等绘图模型
 | 
			
		||||
- 功能优化:Markdown 支持解析 emoji 表情
 | 
			
		||||
- 功能优化:当管理后台禁用了某个绘图菜单的时候,移动端绘图菜单也会同步禁用(不显示该功能)
 | 
			
		||||
 | 
			
		||||
## v4.1.7
 | 
			
		||||
 | 
			
		||||
- Bug 修复:手机邮箱相关的注册问题 [#IB0HS5](https://gitee.com/blackfox/geekai/issues/IB0HS5)
 | 
			
		||||
- Bug 修复:音乐视频无法下载,思维导图下载后看不清文字[#IB0N2E](https://gitee.com/blackfox/geekai/issues/IB0N2E)
 | 
			
		||||
- 功能优化:保存所有 AIGC 任务的原始信息,程序启动之后自动将未执行的任务加入到 redis 队列
 | 
			
		||||
- 功能优化:失败的任务自动退回算力,而不需要在删除的时候再退回
 | 
			
		||||
- 功能新增:支持设置一个专门的模型来翻译提示词,提供 Mate 提示词生成功能
 | 
			
		||||
- Bug 修复:修复图片对话的时候,上下文不起作用的 Bug
 | 
			
		||||
- 功能新增:管理后台新增批量导出兑换码功能
 | 
			
		||||
 | 
			
		||||
## v4.1.6
 | 
			
		||||
 | 
			
		||||
- 功能新增:**支持 OpenAI 实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。
 | 
			
		||||
- 功能优化:优化 MysQL 容器配置文档,解决 MysQL 容器资源占用过高问题
 | 
			
		||||
- 功能新增:管理后台增加 AI 绘图任务管理,可在管理后台浏览和删除用户的绘图任务
 | 
			
		||||
- 功能新增:管理后台增加 Suno 和 Luma 任务管理功能
 | 
			
		||||
- Bug 修复:修复管理后台删除兑换码报 404 错误
 | 
			
		||||
- 功能优化:优化充值产品定价逻辑,可以设置原价和优惠价,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**。
 | 
			
		||||
 | 
			
		||||
## v4.1.5
 | 
			
		||||
 | 
			
		||||
- 功能优化:重构 websocket 组件,减少 websocket 连接数,全站共享一个 websocket 连接
 | 
			
		||||
- Bug 修复:兼容手机端原生微信支付和支付宝支付渠道
 | 
			
		||||
- Bug 修复:修复删除绘图任务时候因为字段长度过短导致 SQL 执行失败问题
 | 
			
		||||
- 功能优化:优化 Vue 组件通信代码,使用共享数据来替换之前的事件订阅模式,效率更高一些
 | 
			
		||||
- 功能优化:优化思维导图生成功果页面,优化用户体验
 | 
			
		||||
 | 
			
		||||
## v4.1.4
 | 
			
		||||
 | 
			
		||||
- 功能优化:用户文件列表组件增加分页功能支持
 | 
			
		||||
- Bug 修复:修复用户注册失败 Bug,注册操作只弹出一次行为验证码
 | 
			
		||||
- 功能优化:首次登录不需要验证码,直接登录,登录失败之后才弹出验证码
 | 
			
		||||
- 功能新增:给 AI 应用(角色)增加分类,前端支持分类筛选
 | 
			
		||||
- 功能优化:允许用户在聊天页面设置是否使用流式输出或者一次性输出,兼容 GPT-O1 模型。
 | 
			
		||||
- 功能优化:移除 PayJS 支付渠道支持,PayJs 已经关闭注册服务,请使用其他支付方式。
 | 
			
		||||
- 功能新增:新增 GeeK 易支付支付渠道,支持支付宝,微信支付,QQ 钱包,京东支付,抖音支付,Paypal 支付等支付方式
 | 
			
		||||
- Bug 修复:修复注册页面 tab 组件没有自动选中问题 [#6](https://github.com/yangjian102621/geekai-plus/issues/6)
 | 
			
		||||
- 功能优化:Luma 生成视频任务增加自动翻译功能
 | 
			
		||||
- Bug 修复:Suno 和 Luma 任务没有判断用户算力
 | 
			
		||||
- 功能新增:邮箱注册增加邮箱后缀白名单,防止使用某些垃圾邮箱注册薅羊毛
 | 
			
		||||
- 功能优化:清空未支付订单时,只清空超过 15 分钟未支付的订单
 | 
			
		||||
 | 
			
		||||
## v4.1.3
 | 
			
		||||
 | 
			
		||||
- 功能优化:重构用户登录模块,给所有的登录组件增加行为验证码功能,支持用户绑定手机,邮箱和微信
 | 
			
		||||
- 功能优化:重构找回密码模块,支持通过手机或者邮箱找回密码
 | 
			
		||||
- 功能优化:管理后台给可以拖动排序的组件添加拖动图标
 | 
			
		||||
- 功能优化:Suno 支持合成完整歌曲,和上传自己的音乐作品进行二次创作
 | 
			
		||||
- Bug 修复:手机端角色和模型选择不生效
 | 
			
		||||
- Bug 修复:用户登录过期之后聊天页面出现大量报错,需要刷新页面才能正常
 | 
			
		||||
- 功能优化:优化聊天页面 Websocket 断线重连代码,提高用户体验
 | 
			
		||||
- 功能优化:给算力增减服务全部加上数据库事务和同步锁
 | 
			
		||||
- 功能优化:支持用户在前端对话界面选择插件
 | 
			
		||||
- 功能新增:支持 Luma 文生视频功能
 | 
			
		||||
 | 
			
		||||
## v4.1.2
 | 
			
		||||
 | 
			
		||||
- Bug 修复:修复思维导图页面获取模型失败的问题
 | 
			
		||||
- 功能优化:优化 MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力
 | 
			
		||||
- Bug 修复:修复后台拖动排序组件 Bug
 | 
			
		||||
- 功能优化:更新数据库失败时候显示具体的的报错信息
 | 
			
		||||
- Bug 修复:修复管理后台对话详情页内容显示异常问题
 | 
			
		||||
- 功能优化:管理后台新增清空所有未支付订单的功能
 | 
			
		||||
- 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求
 | 
			
		||||
- 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力
 | 
			
		||||
 | 
			
		||||
## v4.1.1
 | 
			
		||||
 | 
			
		||||
- Bug 修复:修复 GPT 模型 function call 调用后没有输出的问题
 | 
			
		||||
- 功能新增:允许获取 License 授权用户可以自定义版权信息
 | 
			
		||||
- 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件
 | 
			
		||||
- 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
 | 
			
		||||
- 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
 | 
			
		||||
- 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
 | 
			
		||||
- 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式
 | 
			
		||||
- 功能新增:允许在管理后台设置首页显示的导航菜单
 | 
			
		||||
- Bug 修复:修复注册页面先显示关闭注册组件,然后再显示注册组件
 | 
			
		||||
- 功能新增:增加 Suno 文生歌曲功能
 | 
			
		||||
- 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加
 | 
			
		||||
- 功能优化:在所有列表页面增加返回顶部按钮
 | 
			
		||||
 | 
			
		||||
## v4.1.0
 | 
			
		||||
 | 
			
		||||
- bug 修复:修复移动端修改聊天标题不生效的问题
 | 
			
		||||
- Bug 修复:修复用户注册不显示用户名的问题
 | 
			
		||||
- Bug 修复:修复管理后台拖动排序不生效的问题
 | 
			
		||||
- 功能优化:允许用户设置自定义首页背景图片
 | 
			
		||||
- 功能新增:**支持 AI 解读 PDF, Word, Excel 等文件**
 | 
			
		||||
- 功能优化:优化聊天界面的用户上传文件的列表样式
 | 
			
		||||
- 功能优化:优化聊天页面对话样式,支持列表样式和对话样式切换
 | 
			
		||||
- 功能新增:支持微信扫码登录,未注册用户微信扫码后会自动注册并登录。移动使用微信浏览器打开可以实现无感登录。
 | 
			
		||||
 | 
			
		||||
## v4.0.9
 | 
			
		||||
 | 
			
		||||
- 环境升级:升级 Golang 到 go1.22.4
 | 
			
		||||
- 功能增加:接入微信商户号支付渠道
 | 
			
		||||
- Bug 修复:修复前端页面菜单把页面撑开,底部留白问题
 | 
			
		||||
- 功能优化:聊天页面自动根据内容调整输入框的高度
 | 
			
		||||
- Bug 修复:修复 Dalle 绘图失败退回算力的问题
 | 
			
		||||
- 功能优化:邀请码注册时被邀请人也可以获得赠送的算力
 | 
			
		||||
- 功能优化:允许设置邮件验证码的抬头
 | 
			
		||||
- Bug 修复:修复免费模型不会记录聊天记录的 bug
 | 
			
		||||
- Bug 修复:修复聊天输入公式显示异常的 Bug
 | 
			
		||||
 | 
			
		||||
## v4.0.8
 | 
			
		||||
 | 
			
		||||
- 功能优化:升级 mathjax 公式解析插件,修复公式因为图片访问限制而无法显示的问题
 | 
			
		||||
- 功能优化:当数据库更新失败的时候记录错误日志
 | 
			
		||||
- 功能优化:聊天输入框会随着输入内容的增多自动调整高度
 | 
			
		||||
- Bug 修复:修复移动端聊天页面模型切换不生效的 Bug
 | 
			
		||||
- 功能优化:给 PC 端扫码支付增加签名验证和有效期验证
 | 
			
		||||
- Bug 修复:修复支付码生成 API 权限控制的问题
 | 
			
		||||
- Bug 修复:模型算力设置为 0 时,不扣减用户算力,并且不记录算力消费日志
 | 
			
		||||
- 功能优化:新增随机背景配置项,可以在后台设置,首页使用 Bing 壁纸作为背景图片
 | 
			
		||||
- 功能新增:H5 端支持 Dalle 绘图
 | 
			
		||||
 | 
			
		||||
## v4.0.7
 | 
			
		||||
 | 
			
		||||
- 功能优化:添加导航菜单的时候支持框入外部链接,并支持上传自定义菜单图片
 | 
			
		||||
- Bug 修复:修复弹窗等于图形验证码一直验证失败的问题
 | 
			
		||||
- 功能重构:重构前端 UI 页面,增加顶部导航
 | 
			
		||||
- 功能优化:优化 Vue 非父子组件之间的通信方式
 | 
			
		||||
- 功能优化:优化 ItemList 组件,自动根据页面宽度计算 cols 数量
 | 
			
		||||
 | 
			
		||||
## v4.0.6
 | 
			
		||||
 | 
			
		||||
- Bug 修复:修复 PC 端画廊页面的瀑布流组件样式错乱问题
 | 
			
		||||
- 功能新增:给思维导图增加 ToolBar,实现思维导图的放大缩小和定位
 | 
			
		||||
- Bug 修复:修复思维导图不扣费的 Bug
 | 
			
		||||
- Bug 修复:修复管理后台角色删除失败的 Bug
 | 
			
		||||
- Bug 修复:兼容最新版秋叶 SD 懒人包的 SD API,新增 scheduler 参数
 | 
			
		||||
- 功能优化:支持在管理后台配置 AI 绘图相关配置,包括 SD, MJ-PLUS, MJ-PROXY
 | 
			
		||||
- Bug 修复:修复注册用户提示注册人数达到上限的 Bug
 | 
			
		||||
- 功能优化:将 MJ,SD,Dall 绘画页面的任务列表全改成瀑布流组件
 | 
			
		||||
 | 
			
		||||
## v4.0.5
 | 
			
		||||
 | 
			
		||||
- 功能优化:已授权系统在后台显示授权信息
 | 
			
		||||
- 功能优化:使用思维链提示词生成思维导图,确保生成的思维导图不会出现格式错误
 | 
			
		||||
- 功能优化:优化首页登录注册页面的 UI
 | 
			
		||||
- BUG 修复:修复 License 验证的逻辑漏洞
 | 
			
		||||
- Bug 修复:后台添加用户的时候密码规则限制跟前台注册保持一致
 | 
			
		||||
- 功能新增:管理后台支持切换主题,支持 light 和 dark 两种主题
 | 
			
		||||
- 功能新增:移动端新增 DALL-E 绘画功能
 | 
			
		||||
- 功能新增:新增移动端首页功能,移动端支持 light 和 dark 两种主题
 | 
			
		||||
- 功能新增:移动支持免登录预览功能
 | 
			
		||||
- Bug 修复:解决在同一个浏览器开启多个对话时候对话内容会相互乱串的问题
 | 
			
		||||
- Bug 修复:修复部分中转 API 模型会出现第一输出的字符被淹没的 Bug
 | 
			
		||||
 | 
			
		||||
## v4.0.4
 | 
			
		||||
 | 
			
		||||
- Bug 修复:修复统一千问第二句不回复的问题
 | 
			
		||||
- 功能优化:MJ 和 SD 任务正在执行时不更新已完成任务列表,加快页面渲染速度
 | 
			
		||||
- 功能新增:Dalle AI 绘画功能实现
 | 
			
		||||
- Bug 修复:修复思维导图格式乱码问题
 | 
			
		||||
- 功能优化:支持使用 TLS 邮件协议,解决国内服务器无法使用 25 号端口发送邮件的问题
 | 
			
		||||
- 功能新增:支持从应用列表直接和某个应用对话
 | 
			
		||||
- 功能优化:优化算力日志的页面和首页的 UI
 | 
			
		||||
- 功能新增:支持思维导图导出 PNG 图片下载
 | 
			
		||||
 | 
			
		||||
## v4.0.3
 | 
			
		||||
 | 
			
		||||
- 功能新增:允许为角色应用绑定模型,如指定某个角色只能使用某个模型
 | 
			
		||||
- Bug 修复:兼容 gpt-4-turbo-2024-04-09 模型的函数调用 Bug
 | 
			
		||||
- Bug 修复:修复 MidJourney 在任务超时后出现后面的任务覆盖前面任务的问题
 | 
			
		||||
- 功能新增:支持上传图片和视觉模型
 | 
			
		||||
- 功能优化:优化聊天页面的复制代码按钮样式乱码
 | 
			
		||||
- 功能新增:增加思维导图功能,支持选择不同的对话模型来生成思维导图
 | 
			
		||||
- 功能新增:支持为角色绑定对话模型,比如绑定某个角色只能用 GPT3.5 或者 GPT4
 | 
			
		||||
- 功能新增:支持为模型绑定 API KEY,比如为 GPT3.5 模型绑定免费的 API KEY 给用户免费使用来引流不至于消耗你的收费 KEY。
 | 
			
		||||
- 功能新增:支持管理后台 Logo 修改
 | 
			
		||||
 | 
			
		||||
## 4.0.2
 | 
			
		||||
 | 
			
		||||
- 功能新增:支持前端菜单可以配置
 | 
			
		||||
- 功能优化:在登录和注册界面标题显示软件版本号
 | 
			
		||||
- 功能优化:MJ 绘画支持 --sref 和 --cref 图片一致性参数
 | 
			
		||||
- 功能优化:使用 leveldb 解决 SD 绘图进度图片预览问题
 | 
			
		||||
- Bug 修复:解决因为图片上传使用相对路径而导致融图失败的问题。
 | 
			
		||||
- 功能新增:手机端支持 Stable-Diffusion 绘画
 | 
			
		||||
- 功能新增:管理后台登录页面增加行为验证码,防止爆破
 | 
			
		||||
 | 
			
		||||
## v4.0.1
 | 
			
		||||
 | 
			
		||||
- 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口,SDAPI 兼容各种 stable-diffusion
 | 
			
		||||
  发行版,稳定性更强一些
 | 
			
		||||
- 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API,兼容
 | 
			
		||||
  MJ-Plus 中转
 | 
			
		||||
- 功能新增:用户算力消费日志增加统计功能,统计一段时间内用户消费的算力
 | 
			
		||||
- Bug 修复:修复 iphone 手机无法通过图形验证码的 Bug,使用滑动验证码替换
 | 
			
		||||
- Bug 修复:修复手机端 MidJourney 绘画页面滚动条无法滚动的 Bug
 | 
			
		||||
 | 
			
		||||
## v4.0.0
 | 
			
		||||
 | 
			
		||||
非兼容版本,重大重构,引入算力概念,将系统中所有的能力(AI 对话,MJ 绘画,SD 绘画,DALL 绘画)全部使用算力来兑换。
 | 
			
		||||
只要你的算力值余额不为 0,你就可以进行任何操作。比如一次 GPT3.5 对话消耗 1 个单位算力,一次 GPT4 对话消耗 10 个算力。一次 MJ
 | 
			
		||||
对话消耗 15 个算力...
 | 
			
		||||
 | 
			
		||||
- 功能重构:重构整体系统,全部采用算力来进行结算
 | 
			
		||||
- 功能优化:SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽
 | 
			
		||||
- 功能优化:移动端聊天页面图片支持预览和放大功能
 | 
			
		||||
- 功能优化:MJ 和 SD 页面数据分页加载,解决一次性加载太多数据导致页面卡顿的问题
 | 
			
		||||
- 功能优化:**PC 端不登录也可以预览功能,只有在发起操作的时候才需要登录**
 | 
			
		||||
- 功能优化:控制台订单管理页面显示未支付订单,并提供订单删除功能
 | 
			
		||||
- 功能新增:支持 H5 支付
 | 
			
		||||
- 功能优化:支持数学公式的识别和美化输出
 | 
			
		||||
- 功能新增:新增算力消费日志功能
 | 
			
		||||
- 功能优化:整合 XXL-JOB 实现订单清理,每日算力派发,VIP 算力重置等任务
 | 
			
		||||
- 功能新增:管理后台新增 7 日内新增用户和新增订单统计
 | 
			
		||||
 | 
			
		||||
## v3.2.7
 | 
			
		||||
 | 
			
		||||
- 功能重构:采用 Vant 重构移动页面,新增 MidJourney 功能
 | 
			
		||||
- 功能优化:优化 PC 端 MidJourney 页面布局,新增融图和换脸功能
 | 
			
		||||
- Bug 修复:修复 issue [
 | 
			
		||||
  管理界面操作用户存在的两个问题](https://github.com/yangjian102621/chatgpt-plus/issues/117#issuecomment-1909201532)
 | 
			
		||||
- 功能优化:在对话和聊天记录表中新增冗余字段 model,存储对话模型
 | 
			
		||||
- Bug 修复:IPhone 手机验证码触摸事件坐标错位 [issue 144](https://github.com/yangjian102621/chatgpt-plus/issues/144)
 | 
			
		||||
- Bug 修复:重新生成按钮功能失效问题
 | 
			
		||||
- Bug 修复:对话输入 HTML 标签不显示的问题
 | 
			
		||||
- 功能优化:gpt-4-all/gpts/midjourney-plus 支持第三方平台的 API KEY
 | 
			
		||||
- 功能新增:新增删除文件功能
 | 
			
		||||
- Bug 修复:解决 MJ-Plus discord 图片下载失败问题,使用第三方平台中转地址下载
 | 
			
		||||
- 功能新增:后台管理新怎对话查看和检索功能
 | 
			
		||||
 | 
			
		||||
## v3.2.6
 | 
			
		||||
 | 
			
		||||
- 功能优化:恢复关闭注册系统配置项,管理员可以在后台关闭用户注册,只允许内部添加账号
 | 
			
		||||
- 功能优化:兼用旧版本微信收款消息解析
 | 
			
		||||
- 功能优化:优化订单扫码支付状态轮询功能,当关闭二维码时取消轮询,节约网络资源
 | 
			
		||||
- 功能新增:新增图片发布功能,画廊只显示用户已发布的图片
 | 
			
		||||
- 功能新增:后台新增配置微信客服二维码,可以上传自己的微信客服二维码
 | 
			
		||||
- 功能新增:新增网站公告,可以在管理后台自定义配置
 | 
			
		||||
- 功能新增:新增阿里通义千问大模型支持
 | 
			
		||||
- Bug 修复:修复 MJ 放大任务失败时候 img_call 会增加的 Bug
 | 
			
		||||
- 功能优化:新增虎皮椒和 PayJS 订单状态校验功能,增加安全性
 | 
			
		||||
- Bug 修复:修复微信转账交易 ID 提取失败 Bug
 | 
			
		||||
- 功能优化:给所有的 websocket 连接加上心跳,解决 "close 1006 (abnormal closure): unexpected EOF" Bug
 | 
			
		||||
- 功能新增:新增短信宝短信平台发送平台集成
 | 
			
		||||
 | 
			
		||||
## v3.2.5
 | 
			
		||||
 | 
			
		||||
- 功能新增:**重磅更新!!!** 新增 MidJourney-Plus API 支持,一秒配置,开箱即用,高效稳定。
 | 
			
		||||
- 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅
 | 
			
		||||
  Plus 账号了!!!
 | 
			
		||||
- 功能优化:增强 markdown 图片和引用块解析。
 | 
			
		||||
- 功能新增:新增用户文件管理,目前一支持上传文件跟 GPT 进行多态对话。
 | 
			
		||||
- 功能优化:function call 兼用中转 API。
 | 
			
		||||
- Bug 修复:修复部分已知的 Bug。
 | 
			
		||||
 | 
			
		||||
## v3.2.4.1
 | 
			
		||||
 | 
			
		||||
- 功能新增:新增 PayJs 支付通道
 | 
			
		||||
- Bug 修复:紧急修复后台添加用户失败问题
 | 
			
		||||
- Bug 修复:紧急修复使用中转 API-KEY 无法绘图的问题
 | 
			
		||||
- Bug 修复:允许用户关闭手机和邮箱注册通道,移除验证码依赖
 | 
			
		||||
 | 
			
		||||
## v3.2.4
 | 
			
		||||
 | 
			
		||||
- 功能新增:重磅更新,支持邮箱注册
 | 
			
		||||
- 功能优化:优化函数调用授权
 | 
			
		||||
- 功能优化:给用户表新增 nickname 字段
 | 
			
		||||
- 功能优化:管理后台给聊天角色增加启用/禁用开关
 | 
			
		||||
- Bug 修复:SD 绘画出现重复扣减绘图次数
 | 
			
		||||
- 功能优化:优化聊天对话导出样式,适应移动端
 | 
			
		||||
- 功能新增:众筹核销可以选择兑换对话还是绘图的额度
 | 
			
		||||
- Bug 修复:修复[从历史记录获取 reply 有并发风险 #92](https://github.com/yangjian102621/chatgpt-plus/issues/92)
 | 
			
		||||
- Bug 修复:修复 MidJourney 绘图任务调度 Bug,为 task_id 建议唯一索引
 | 
			
		||||
- 功能重构:重构了 API KEY 模块,支持为每个 API KEY 都设置不同的 API 地址,并可以单独开启是否使用代理。
 | 
			
		||||
 | 
			
		||||
## v3.2.3
 | 
			
		||||
 | 
			
		||||
- 功能重构:重构函数工具模块,设计成可以后台动态管理函数。支持添加自定义函数实现
 | 
			
		||||
- 功能新增:为充值产品数据表添加 img_calls 字段,支持充值绘图次数
 | 
			
		||||
- Bug 修复:修复 [MJ 机器人空指针异常的 Bug](https://github.com/yangjian102621/chatgpt-plus/issues/73)
 | 
			
		||||
- Bug 修复:确保相同 Prompt 的绘图任务的 Upscale 和 Variation 任务调度给相同的频道
 | 
			
		||||
- 功能新增:新增删除绘图任何和图片功能
 | 
			
		||||
- Bug 修复:修复虎皮椒支付二维码重复扫码时报错问题
 | 
			
		||||
- 功能优化:自动将 AI 绘画中的中文提示词翻译成英文
 | 
			
		||||
- 功能优化:优化 AI 绘画的大图压缩算法,新增图片缓存
 | 
			
		||||
- 功能优化:支持为 MJ 绘图 API 增加反代功能,提高图片的加载速度,大大降低绘图任务的失败率
 | 
			
		||||
- Bug 修复:修复[Azure Api 更换 api-version 参数后请求失败的问题](https://github.com/yangjian102621/chatgpt-plus/pull/71)
 | 
			
		||||
- Bug 修复:修复科大讯飞 V1.5 API 请求失败的问题
 | 
			
		||||
- Bug 修复:绘图失败后,自动恢复用户的剩余绘图次数
 | 
			
		||||
- 功能新增:为移动端新增 SD 绘图功能,分享功能
 | 
			
		||||
 | 
			
		||||
## v3.2.2
 | 
			
		||||
 | 
			
		||||
- 功能重构:重构 MidJourney 和 Stable-Diffusion 绘图模块,支持使用多组配置创建池子提供绘画服务
 | 
			
		||||
- 功能新增:AI 绘画页面增加翻译和重写提示词功能
 | 
			
		||||
- 功能优化:OSS 上传组件支持在 Bucket 下设置二级目录
 | 
			
		||||
- Bug 修复:修复阿里云 OSS 访问路径错误
 | 
			
		||||
- 功能优化:在 AI 绘图页面使用 HTTP 轮询替换 Websocket
 | 
			
		||||
 | 
			
		||||
## v3.2.1
 | 
			
		||||
 | 
			
		||||
- 功能优化:切换角色和模型的时候自动创建新的对话
 | 
			
		||||
- Bug 修复:修复文件上传失败 No such file bug
 | 
			
		||||
- 功能新增:MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数
 | 
			
		||||
- Bug 修复:[PC 端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59)
 | 
			
		||||
- 功能新增:增加 arm64 架构打包脚本
 | 
			
		||||
- 功能新增:支持 dall-e3 绘图的 API 地址自定义配置
 | 
			
		||||
- 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道
 | 
			
		||||
 | 
			
		||||
## v3.2.0
 | 
			
		||||
 | 
			
		||||
- 功能新增:新增邀请注册功能
 | 
			
		||||
- 功能优化:增加中间件自动对 HTTP 请求的参数去掉首尾空格
 | 
			
		||||
- 功能优化:增加中间件自动为大图片生成缩略图
 | 
			
		||||
- 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载
 | 
			
		||||
- 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
 | 
			
		||||
- Bug 修复:修复阿里云 OSS 域名设置不起做用的 bug
 | 
			
		||||
- Bug 修复:修复 MidJourney 绘图失败后重复添加到队列的问题
 | 
			
		||||
 | 
			
		||||
## v3.1.9
 | 
			
		||||
 | 
			
		||||
- 功能新增:增加讯飞星火大模型 v3.0 支持
 | 
			
		||||
- 功能新增:新增找回密码功能
 | 
			
		||||
- 功能新增:支持 Markdown 代码复制功能
 | 
			
		||||
- Bug 修复: xxl-job 任务调度失败的 Bug
 | 
			
		||||
- 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font
 | 
			
		||||
- Bug 修复:Stable-Diffusion 绘画成功之后没有扣减用户画图次数
 | 
			
		||||
- 功能优化:优化会员充值页面 ItemList 组件
 | 
			
		||||
- 功能优化:给首页 Logo 增加链接
 | 
			
		||||
- Bug 修复:[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51)
 | 
			
		||||
- Bug 修复:聊天上下文失效问题
 | 
			
		||||
- 功能优化:关闭注册时显示联系管理员二维码
 | 
			
		||||
- 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能
 | 
			
		||||
- Bug 修复:后台启用用户 VIP 不生效问题
 | 
			
		||||
- 功能优化:充值支付页面的支付说明文字可以后台配置
 | 
			
		||||
- Bug 修复:ChatGLM,百度文心,科大讯飞模型输出代码不换行问题
 | 
			
		||||
 | 
			
		||||
## v3.1.8
 | 
			
		||||
 | 
			
		||||
1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
 | 
			
		||||
2. Bug 修复:修复 MidJourney API 参数版本更新导致调用失败问题
 | 
			
		||||
3. Bug 修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题
 | 
			
		||||
4. Bug 修复:修复七牛云上传报错 expired token
 | 
			
		||||
5. Bug 修复:修复高权重模型导致的对话次数为负数的漏洞
 | 
			
		||||
6. 功能优化:将聊天报错信息定义为统一常量,方便修改
 | 
			
		||||
7. 功能优化:优化 markdown 表格显示样式,覆写 Element-Plus 表格样式
 | 
			
		||||
8. 功能优化:增加倒数计时组件,定期自动清理未支付的订单
 | 
			
		||||
 | 
			
		||||
## v3.1.7
 | 
			
		||||
 | 
			
		||||
1. 功能新增:支持文心 4.0 AI 模型
 | 
			
		||||
2. 功能新增:可以在管理后台为用户绑定指定的 AI 模型,如只给某个用户使用 GPT-4 模型
 | 
			
		||||
3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如 GPT4 是 GPT3.5 的 10 倍
 | 
			
		||||
4. 功能新增:新增系统配置关闭 AI 模型的函数功能
 | 
			
		||||
5. 功能优化:优化 MidJourney 专业绘画页面图片预览样式
 | 
			
		||||
 | 
			
		||||
## v3.1.6
 | 
			
		||||
 | 
			
		||||
1. 功能新增:新增 AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品
 | 
			
		||||
2. 功能新增:新增 AI 角色应用功能页面,用户可以添加自己感兴趣的应用
 | 
			
		||||
3. 功能优化:优化瀑布流组件的页面布局
 | 
			
		||||
4. 功能优化:新注册用户成功之后自动登录
 | 
			
		||||
5. 功能优化:优化更新对话标题的操作体验,绑定回车事件
 | 
			
		||||
 | 
			
		||||
## v3.1.5
 | 
			
		||||
 | 
			
		||||
1. 功能新增:新增百度文心一言大模型 API 接入支持
 | 
			
		||||
2. 功能新增:新增科大讯飞星火大模型 API 接入支持
 | 
			
		||||
3. 功能重构:将 chat_handler 的所有功能实现放入单独的包中
 | 
			
		||||
4. 功能新增:新增系统配置 `enabled_function` 用于启用和关闭函数功能
 | 
			
		||||
5. Bug 修复:修复管理后台更新 API Key 失败的 Bug
 | 
			
		||||
6. Bug 修复:修复新建的对话无法更新对话标题的 Bug
 | 
			
		||||
7. 功能优化:其他一些小的体验优化工作
 | 
			
		||||
 | 
			
		||||
## v3.1.4
 | 
			
		||||
 | 
			
		||||
1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio 和阿里云 OSS 四种存储介质。
 | 
			
		||||
2. 功能新增:**增加 Stable Diffusion 绘画功能页面**。
 | 
			
		||||
3. 功能重构:将 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts) 合并到本项目,部署更加简单,无需部署两个项目了。
 | 
			
		||||
4. Bug 修复:修复[用户注册报错 BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。
 | 
			
		||||
5. Bug 修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。
 | 
			
		||||
6. 功能优化:增加阿里云短信服务配置项 `Sign` 和 `CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。
 | 
			
		||||
7. 功能优化:添加系统配置用来设置自定义的众筹微信收款二维码。
 | 
			
		||||
8. 功能优化:优化绘画页面的弹窗样式和页面布局。
 | 
			
		||||
 | 
			
		||||
## v3.1.3
 | 
			
		||||
 | 
			
		||||
1. 页面重构:重后 Home 页面,拆分成聊天,MJ 绘画,SD 绘画,应用广场等多个功能菜单。
 | 
			
		||||
2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。
 | 
			
		||||
3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。
 | 
			
		||||
4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。
 | 
			
		||||
5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。
 | 
			
		||||
6. Bug 修复:修复 JWT token 有效期计算错误的 Bug。
 | 
			
		||||
 | 
			
		||||
## v3.1.2
 | 
			
		||||
 | 
			
		||||
1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。
 | 
			
		||||
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
 | 
			
		||||
3. Bug 修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
 | 
			
		||||
4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。
 | 
			
		||||
5. 功能优化:取消导出聊天页面的授权验证
 | 
			
		||||
6. 功能优化:所有路由跳转都使用绝对路径
 | 
			
		||||
 | 
			
		||||
## v3.1.1
 | 
			
		||||
 | 
			
		||||
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的 Bug
 | 
			
		||||
 | 
			
		||||
## v3.1.0(大版本更新)
 | 
			
		||||
 | 
			
		||||
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及
 | 
			
		||||
   ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
 | 
			
		||||
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
 | 
			
		||||
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储 handler,目前已经实现本地存储和 minio oss 存储。
 | 
			
		||||
4. 功能优化:更新头像自动删除旧的图片资源。
 | 
			
		||||
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
 | 
			
		||||
6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。
 | 
			
		||||
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
 | 
			
		||||
8. 升级 gin, element-plus,redis 组件到最新版本。
 | 
			
		||||
9. Bug 修复:修复若干已知的的 Bug
 | 
			
		||||
 | 
			
		||||
## v3.0.7
 | 
			
		||||
 | 
			
		||||
1. 聊天主界面:新增聊天引导页面,介绍产品功能
 | 
			
		||||
@@ -8,7 +447,7 @@
 | 
			
		||||
4. 功能新增:支持导出聊天记录为 PDF 文件。
 | 
			
		||||
5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。
 | 
			
		||||
6. 功能优化:支持用户设置默认的 GPT 模型
 | 
			
		||||
7. Bug修复:修复若干已知的的 Bug
 | 
			
		||||
7. Bug 修复:修复若干已知的的 Bug
 | 
			
		||||
 | 
			
		||||
## v3.0.6
 | 
			
		||||
 | 
			
		||||
@@ -16,8 +455,8 @@
 | 
			
		||||
2. 管理后台:新增重置用户密码功能
 | 
			
		||||
3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景
 | 
			
		||||
4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗
 | 
			
		||||
5. Bug修复:修复注册页面验证码不显示 Bug
 | 
			
		||||
6. Bug修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
 | 
			
		||||
5. Bug 修复:修复注册页面验证码不显示 Bug
 | 
			
		||||
6. Bug 修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
 | 
			
		||||
7. 功能修正:允许用户使用手机号码登录
 | 
			
		||||
8. 功能优化:更新系统配置后同步更新服务端内存变量数据
 | 
			
		||||
9. 功能优化:优化打包脚本,减少容器镜像大小
 | 
			
		||||
@@ -75,5 +514,5 @@
 | 
			
		||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
 | 
			
		||||
5. 保存聊天记录,支持聊天上下文。
 | 
			
		||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
 | 
			
		||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
 | 
			
		||||
8. 支持会话搜索过滤。
 | 
			
		||||
7. 引入 ip2region 组件,记录用户的登录 IP 和地址。
 | 
			
		||||
8. 支持会话搜索过滤。
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										346
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										346
									
								
								README.md
									
									
									
									
									
								
							@@ -1,329 +1,91 @@
 | 
			
		||||
# ChatGPT-Plus
 | 
			
		||||
# GeekAI
 | 
			
		||||
 | 
			
		||||
**ChatGPT-PLUS** 是基于 OpenAI API 实现的 ChatGPT 聊天系统。主要有如下特性:
 | 
			
		||||
> 根据[《生成式人工智能服务管理暂行办法》](https://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
 | 
			
		||||
 | 
			
		||||
* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
 | 
			
		||||
* 聊天体验跟 ChatGPT 官方版本完全一致。
 | 
			
		||||
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
 | 
			
		||||
**GeekAI** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Claude, 通义千问,Kimi,DeepSeek,Gitee AI 等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI 绘画功能。
 | 
			
		||||
 | 
			
		||||
主要特性:
 | 
			
		||||
 | 
			
		||||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
 | 
			
		||||
- 基于 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/plus/info/)。
 | 
			
		||||
 | 
			
		||||
### PC 端聊天界面
 | 
			
		||||
### 体验地址
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
> 免费体验地址:[https://chat.geekai.me](https://chat.geekai.me) <br/> > **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
 | 
			
		||||
 | 
			
		||||
### 新版聊天界面
 | 
			
		||||
## 快速部署
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 自动调用函数插件
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 用户设置
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 登录页面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 管理后台
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 移动端 Web 页面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 7. 体验地址
 | 
			
		||||
 | 
			
		||||
> 免费体验地址:[https://ai.r9it.com/chat](https://ai.r9it.com/chat) <br/>
 | 
			
		||||
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
 | 
			
		||||
请参考文档 [**GeekAI 快速部署**](https://docs.geekai.me/plus/install/)。
 | 
			
		||||
 | 
			
		||||
## 使用须知
 | 
			
		||||
 | 
			
		||||
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
 | 
			
		||||
1. 本项目基于 Apache2.0 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
 | 
			
		||||
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
 | 
			
		||||
 | 
			
		||||
## 项目介绍
 | 
			
		||||
 | 
			
		||||
这一套完整的系统,包括前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 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. 保存聊天记录,支持聊天上下文。
 | 
			
		||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
 | 
			
		||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
 | 
			
		||||
8. 支持会话搜索过滤。
 | 
			
		||||
9. 支持微信支付充值
 | 
			
		||||
 | 
			
		||||
## 项目地址
 | 
			
		||||
 | 
			
		||||
* 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
 | 
			
		||||
* [ ] 开发移动端聊天页面
 | 
			
		||||
* [x] 接入微信支付功能
 | 
			
		||||
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
 | 
			
		||||
* [ ] 接入语音和 TTS API,支持语音聊天
 | 
			
		||||
* [ ] 开发手机 App 客户端
 | 
			
		||||
- [ ] 支持基于知识库的 AI 问答
 | 
			
		||||
- [ ] 文生视频,文生歌曲功能
 | 
			
		||||
- [ ] 微信支付功能
 | 
			
		||||
 | 
			
		||||
## Docker 快速部署
 | 
			
		||||
## 项目文档
 | 
			
		||||
 | 
			
		||||
>
 | 
			
		||||
鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。
 | 
			
		||||
> 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。
 | 
			
		||||
最新的部署视频教程:[https://www.bilibili.com/video/BV1Cc411t7CX/](https://www.bilibili.com/video/BV1Cc411t7CX/)
 | 
			
		||||
 | 
			
		||||
V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
 | 
			
		||||
详细的部署和开发文档请参考 [**GeekAI 文档**](https://docs.geekai.me)。
 | 
			
		||||
 | 
			
		||||
### 1. 导入数据库
 | 
			
		||||
加微信进入微信讨论群可获取 **一键部署脚本(添加好友时请注明来自 Github!!!)。**
 | 
			
		||||
 | 
			
		||||
首先我们需要创建一个 MySQL 容器,并导入初始数据库。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd docker/mysql
 | 
			
		||||
# 创建 mysql 容器
 | 
			
		||||
docker-compose up -d
 | 
			
		||||
# 导入数据库
 | 
			
		||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus.sql
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
# 连接数据库
 | 
			
		||||
mysql -u username -p password
 | 
			
		||||
# 导入数据库
 | 
			
		||||
source database/chatgpt_plus.sql
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. 修改配置文档
 | 
			
		||||
 | 
			
		||||
修改配置文档 `docker/conf/config.toml` 配置文档,修改代理地址和管理员密码:
 | 
			
		||||
 | 
			
		||||
```toml
 | 
			
		||||
Listen = "0.0.0.0:5678"
 | 
			
		||||
ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理,如:http://127.0.0.1:7777
 | 
			
		||||
#ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了
 | 
			
		||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/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" # 如果是生产环境的话,这里管理员的密码记得修改
 | 
			
		||||
  
 | 
			
		||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
 | 
			
		||||
  ApiURL = "{URL}"
 | 
			
		||||
  AppId = "{APP_ID}"
 | 
			
		||||
  Token = "{TOKEN}"
 | 
			
		||||
 | 
			
		||||
[SmsConfig] # 阿里云短信服务配置
 | 
			
		||||
  AccessKey = "{YOUR_ACCESS_KEY}"
 | 
			
		||||
  AccessSecret = "{YOUR_SECRET_KEY}"
 | 
			
		||||
  Product = "Dysmsapi"
 | 
			
		||||
  Domain = "dysmsapi.aliyuncs.com"
 | 
			
		||||
 | 
			
		||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
 | 
			
		||||
  ApiURL = "插件扩展 API 地址"
 | 
			
		||||
  Token = "插件扩展 API Token" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行 
 | 
			
		||||
  
 | 
			
		||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
 | 
			
		||||
   Active = "local" # 默认使用本地文件存储
 | 
			
		||||
   [OSS.Local]
 | 
			
		||||
     BasePath = "./static/upload" # 本地文件上传根路径
 | 
			
		||||
     BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
 | 
			
		||||
   [OSS.Minio]
 | 
			
		||||
     Endpoint = "IP:端口" # 如 172.22.11.200:9000
 | 
			
		||||
     AccessKey = "minio oss access key" # 自己去 Minio 控制台去创建一个 Access Key
 | 
			
		||||
     AccessSecret = "minio oss access secret"
 | 
			
		||||
     Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
 | 
			
		||||
     UseSSL = false
 | 
			
		||||
     Domain = "minio 文件公开访问地址" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> 如果要启用微信收款服务和 MidJourney
 | 
			
		||||
> 绘画功能,请先部署扩展服务项目 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts)。
 | 
			
		||||
 | 
			
		||||
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
 # 这里配置后端 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://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 3. 启动应用
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd docker
 | 
			
		||||
docker-compose up -d
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* 前端访问地址:http://localhost:8080/chat
 | 
			
		||||
* 后台管理地址:http://localhost:8080/admin
 | 
			
		||||
* 移动端地址:http://localhost:8080/mobile
 | 
			
		||||
 | 
			
		||||
> 注意:你得访问后台管理系统 http://localhost:8080/admin
 | 
			
		||||
> 输入你前面配置文档中设置的管理员用户名和密码登录。
 | 
			
		||||
> 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
最后登录前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
 | 
			
		||||
你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。
 | 
			
		||||
 | 
			
		||||
祝你使用愉快!!!
 | 
			
		||||
 | 
			
		||||
## 本地开发调试
 | 
			
		||||
 | 
			
		||||
本地开发同样要分别运行前端和后端程序。
 | 
			
		||||
 | 
			
		||||
### 运行后端程序
 | 
			
		||||
 | 
			
		||||
1. 同样你首先要 [导入数据库](#1-导入数据库)
 | 
			
		||||
2. 然后 [修改配置文档](#2-修改配置文档)
 | 
			
		||||
3. 运行后端程序:
 | 
			
		||||
 | 
			
		||||
    ```shell
 | 
			
		||||
    cd api 
 | 
			
		||||
    # 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
 | 
			
		||||
# for all platforms
 | 
			
		||||
make clean all
 | 
			
		||||
# for linux only
 | 
			
		||||
make clean linux
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
打包后的可执行文件在 `bin` 目录下。
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 参与贡献
 | 
			
		||||
 | 
			
		||||
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
 | 
			
		||||
 | 
			
		||||
如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。
 | 
			
		||||
 | 
			
		||||

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

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								api/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								api/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -17,4 +17,5 @@ bin
 | 
			
		||||
data
 | 
			
		||||
config.toml
 | 
			
		||||
static/upload 
 | 
			
		||||
storage.json
 | 
			
		||||
storage.json 
 | 
			
		||||
res/certs/wechat/apiclient_key.pem
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								api/Makefile
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								api/Makefile
									
									
									
									
									
								
							@@ -1,19 +1,14 @@
 | 
			
		||||
SHELL=/usr/bin/env bash
 | 
			
		||||
NAME := chatgpt-v3
 | 
			
		||||
all: window linux darwin
 | 
			
		||||
NAME := geekai
 | 
			
		||||
all: amd64 arm64
 | 
			
		||||
 | 
			
		||||
amd64:
 | 
			
		||||
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/$(NAME)-linux main.go
 | 
			
		||||
.PHONY: amd64
 | 
			
		||||
 | 
			
		||||
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=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
 | 
			
		||||
.PHONY: darwin
 | 
			
		||||
arm64:
 | 
			
		||||
	CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build  -ldflags "-s -w" -o bin/$(NAME)-linux main.go
 | 
			
		||||
.PHONY: arm64
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
	rm -rf bin/$(NAME)-*
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +1,115 @@
 | 
			
		||||
Listen = "0.0.0.0:5678"
 | 
			
		||||
ProxyURL = "http://172.22.11.200:7777"
 | 
			
		||||
MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
 | 
			
		||||
StaticDir = "./static"
 | 
			
		||||
StaticUrl = "http://localhost:5678/static"
 | 
			
		||||
AesEncryptKey = "{YOUR_AES_KEY}"
 | 
			
		||||
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]
 | 
			
		||||
  Driver = "cookie"
 | 
			
		||||
  SecretKey = "m0cjm3gsuw9jk73np1ni7r42koilybjcndlycjdmq7za3pbqn7w12fyok5pqh6q5"
 | 
			
		||||
  Name = "CHAT_SESSION_ID"
 | 
			
		||||
  Path = "/"
 | 
			
		||||
  Domain = "localhost"
 | 
			
		||||
  SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
 | 
			
		||||
  MaxAge = 86400
 | 
			
		||||
  Secure = false
 | 
			
		||||
  HttpOnly = false
 | 
			
		||||
  SameSite = 2
 | 
			
		||||
 | 
			
		||||
[Manager]
 | 
			
		||||
  Username = "admin"
 | 
			
		||||
  Password = "admin123"
 | 
			
		||||
 | 
			
		||||
[Redis]
 | 
			
		||||
[Redis] # redis 配置信息
 | 
			
		||||
  Host = "localhost"
 | 
			
		||||
  Port = 6379
 | 
			
		||||
  Password = ""
 | 
			
		||||
  DB = 0
 | 
			
		||||
 | 
			
		||||
[ApiConfig]
 | 
			
		||||
  ApiURL = "{URL}"
 | 
			
		||||
  AppId = "{APP_ID}"
 | 
			
		||||
  Token = "{TOKEN}"
 | 
			
		||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
 | 
			
		||||
  ApiURL = "https://sapi.geekai.me"
 | 
			
		||||
  AppId = ""
 | 
			
		||||
  Token = ""
 | 
			
		||||
 | 
			
		||||
[SmsConfig]
 | 
			
		||||
  AccessKey = "{YOUR_ACCESS_KEY}"
 | 
			
		||||
  AccessSecret = "{YOUR_SECRET_KEY}"
 | 
			
		||||
  Product = "Dysmsapi"
 | 
			
		||||
  Domain = "dysmsapi.aliyuncs.com"
 | 
			
		||||
 | 
			
		||||
[ExtConfig]
 | 
			
		||||
  ApiURL = "插件扩展 API 地址"
 | 
			
		||||
  Token = "插件扩展 API 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]
 | 
			
		||||
   Active = "local"
 | 
			
		||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
 | 
			
		||||
   Active = "local" # 默认使用本地文件存储引擎
 | 
			
		||||
   [OSS.Local]
 | 
			
		||||
     BasePath = "./static/upload"
 | 
			
		||||
     BaseURL = "http://localhost:5678/static/upload"
 | 
			
		||||
     BasePath = "./static/upload" # 本地文件上传根路径
 | 
			
		||||
     BaseURL = "http://localhost:5678/static/upload" # 本地上传文件前缀 URL,线上需要把 localhost 替换成自己的实际域名或者IP
 | 
			
		||||
   [OSS.Minio]
 | 
			
		||||
     Endpoint = "IP:端口"
 | 
			
		||||
     AccessKey = "minio oss access key"
 | 
			
		||||
     AccessSecret = "minio oss access secret"
 | 
			
		||||
     Bucket = "chatgpt-plus"
 | 
			
		||||
     Endpoint = "" # 如 172.22.11.200:9000
 | 
			
		||||
     AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
 | 
			
		||||
     AccessSecret = ""
 | 
			
		||||
     Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
 | 
			
		||||
     UseSSL = false
 | 
			
		||||
     Domain = "minio 文件公开访问地址"
 | 
			
		||||
     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"] # 支持的支付方式
 | 
			
		||||
 
 | 
			
		||||
@@ -1,93 +1,100 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/function"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-contrib/sessions/cookie"
 | 
			
		||||
	"github.com/gin-contrib/sessions/memstore"
 | 
			
		||||
	"github.com/gin-contrib/sessions/redis"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"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
 | 
			
		||||
	ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message
 | 
			
		||||
 | 
			
		||||
	ChatConfig *types.ChatConfig   // chat config cache
 | 
			
		||||
	SysConfig  *types.SystemConfig // system config cache
 | 
			
		||||
 | 
			
		||||
	// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
 | 
			
		||||
	// 防止第三方直接连接 socket 调用 OpenAI API
 | 
			
		||||
	ChatSession   *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
 | 
			
		||||
	ChatClients   *types.LMap[string, *types.WsClient]    // map[sessionId]Websocket 连接集合
 | 
			
		||||
	ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
 | 
			
		||||
	Functions     map[string]function.Function
 | 
			
		||||
	MjTaskClients *types.LMap[string, *types.WsClient]
 | 
			
		||||
	Debug     bool
 | 
			
		||||
	Config    *types.AppConfig
 | 
			
		||||
	Engine    *gin.Engine
 | 
			
		||||
	SysConfig *types.SystemConfig // system config cache
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServer(appConfig *types.AppConfig, functions map[string]function.Function) *AppServer {
 | 
			
		||||
func NewServer(appConfig *types.AppConfig) *AppServer {
 | 
			
		||||
	gin.SetMode(gin.ReleaseMode)
 | 
			
		||||
	gin.DefaultWriter = io.Discard
 | 
			
		||||
	return &AppServer{
 | 
			
		||||
		Debug:         false,
 | 
			
		||||
		Config:        appConfig,
 | 
			
		||||
		Engine:        gin.Default(),
 | 
			
		||||
		ChatContexts:  types.NewLMap[string, []interface{}](),
 | 
			
		||||
		ChatSession:   types.NewLMap[string, *types.ChatSession](),
 | 
			
		||||
		ChatClients:   types.NewLMap[string, *types.WsClient](),
 | 
			
		||||
		ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
 | 
			
		||||
		MjTaskClients: types.NewLMap[string, *types.WsClient](),
 | 
			
		||||
		Functions:     functions,
 | 
			
		||||
		Debug:  false,
 | 
			
		||||
		Config: appConfig,
 | 
			
		||||
		Engine: gin.Default(),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *AppServer) Init(debug bool) {
 | 
			
		||||
	if debug { // 调试模式允许跨域请求 API
 | 
			
		||||
		s.Debug = debug
 | 
			
		||||
		logger.Info("Enabled debug mode")
 | 
			
		||||
	}
 | 
			
		||||
func (s *AppServer) Init(debug bool, client *redis.Client) {
 | 
			
		||||
	// 允许跨域请求 API
 | 
			
		||||
	s.Engine.Use(corsMiddleware())
 | 
			
		||||
	s.Engine.Use(sessionMiddleware(s.Config))
 | 
			
		||||
	s.Engine.Use(authorizeMiddleware(s))
 | 
			
		||||
	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 chat config from database
 | 
			
		||||
	var chatConfig model.Config
 | 
			
		||||
	res := db.Where("marker", "chat").First(&chatConfig)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return res.Error
 | 
			
		||||
	}
 | 
			
		||||
	err := utils.JsonDecode(chatConfig.Config, &s.ChatConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// load system configs
 | 
			
		||||
	var sysConfig model.Config
 | 
			
		||||
	res = db.Where("marker", "system").First(&sysConfig)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return res.Error
 | 
			
		||||
	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 err
 | 
			
		||||
		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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -97,7 +104,7 @@ func errorHandler(c *gin.Context) {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			logger.Errorf("Handler Panic: %v", r)
 | 
			
		||||
			debug.PrintStack()
 | 
			
		||||
			c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
 | 
			
		||||
			c.JSON(http.StatusBadRequest, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
 | 
			
		||||
			c.Abort()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
@@ -105,42 +112,6 @@ func errorHandler(c *gin.Context) {
 | 
			
		||||
	c.Next()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 会话处理
 | 
			
		||||
func sessionMiddleware(config *types.AppConfig) gin.HandlerFunc {
 | 
			
		||||
	// encrypt the cookie
 | 
			
		||||
	var store sessions.Store
 | 
			
		||||
	var err error
 | 
			
		||||
	switch config.Session.Driver {
 | 
			
		||||
	case types.SessionDriverMem:
 | 
			
		||||
		store = memstore.NewStore([]byte(config.Session.SecretKey))
 | 
			
		||||
		break
 | 
			
		||||
	case types.SessionDriverRedis:
 | 
			
		||||
		store, err = redis.NewStore(10, "tcp", config.Redis.Url(), config.Redis.Password, []byte(config.Session.SecretKey))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	case types.SessionDriverCookie:
 | 
			
		||||
		store = cookie.NewStore([]byte(config.Session.SecretKey))
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		config.Session.Driver = types.SessionDriverCookie
 | 
			
		||||
		store = cookie.NewStore([]byte(config.Session.SecretKey))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Info("Session driver: ", config.Session.Driver)
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
@@ -151,9 +122,9 @@ func corsMiddleware() gin.HandlerFunc {
 | 
			
		||||
			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, Chat-Token")
 | 
			
		||||
			c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
 | 
			
		||||
			// 允许浏览器(客户端)可以解析的头部 (重要)
 | 
			
		||||
			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
 | 
			
		||||
			c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
 | 
			
		||||
			//设置缓存时间
 | 
			
		||||
			c.Header("Access-Control-Max-Age", "172800")
 | 
			
		||||
			//允许客户端传递校验信息比如 cookie (重要)
 | 
			
		||||
@@ -175,44 +146,253 @@ func corsMiddleware() gin.HandlerFunc {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 用户授权验证
 | 
			
		||||
func authorizeMiddleware(s *AppServer) gin.HandlerFunc {
 | 
			
		||||
func authorizeMiddleware(s *AppServer, client *redis.Client) 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/reward/notify" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/mj/notify" ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/static/") ||
 | 
			
		||||
			c.Request.URL.Path == "/api/admin/config/get" {
 | 
			
		||||
		clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
 | 
			
		||||
		var tokenString string
 | 
			
		||||
		isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
 | 
			
		||||
		if isAdminApi { // 后台管理 API
 | 
			
		||||
			tokenString = c.GetHeader(types.AdminAuthHeader)
 | 
			
		||||
		} else if clientProtocols != "" { // Websocket 连接
 | 
			
		||||
			// 解析子协议内容
 | 
			
		||||
			protocols := strings.Split(clientProtocols, ",")
 | 
			
		||||
			if protocols[0] == "realtime" {
 | 
			
		||||
				tokenString = strings.TrimSpace(protocols[1][25:])
 | 
			
		||||
			} else if protocols[0] == "token" {
 | 
			
		||||
				tokenString = strings.TrimSpace(protocols[1])
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			tokenString = c.GetHeader(types.UserAuthHeader)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if tokenString == "" {
 | 
			
		||||
			if needLogin(c) {
 | 
			
		||||
				resp.NotAuth(c, "You should put Authorization in request headers")
 | 
			
		||||
				c.Abort()
 | 
			
		||||
				return
 | 
			
		||||
			} else { // 直接放行
 | 
			
		||||
				c.Next()
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
 | 
			
		||||
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok && needLogin(c) {
 | 
			
		||||
				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
 | 
			
		||||
			}
 | 
			
		||||
			if isAdminApi {
 | 
			
		||||
				return []byte(s.Config.AdminSession.SecretKey), nil
 | 
			
		||||
			} else {
 | 
			
		||||
				return []byte(s.Config.Session.SecretKey), nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		if err != nil && needLogin(c) {
 | 
			
		||||
			resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		claims, ok := token.Claims.(jwt.MapClaims)
 | 
			
		||||
		if !ok || !token.Valid && needLogin(c) {
 | 
			
		||||
			resp.NotAuth(c, "Token is invalid")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
 | 
			
		||||
		if expr > 0 && int64(expr) < time.Now().Unix() && needLogin(c) {
 | 
			
		||||
			resp.NotAuth(c, "Token is expired")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		key := fmt.Sprintf("users/%v", claims["user_id"])
 | 
			
		||||
		if isAdminApi {
 | 
			
		||||
			key = fmt.Sprintf("admin/%v", claims["user_id"])
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := client.Get(context.Background(), key).Result(); err != nil && needLogin(c) {
 | 
			
		||||
			resp.NotAuth(c, "Token is not found in redis")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		c.Set(types.LoginUserID, claims["user_id"])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func needLogin(c *gin.Context) bool {
 | 
			
		||||
	if c.Request.URL.Path == "/api/user/login" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/user/logout" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/user/resetPass" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/admin/login" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/admin/logout" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/admin/login/captcha" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/user/register" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/chat/history" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/chat/detail" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/chat/list" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/app/list" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/app/type/list" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/app/list/user" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/model/list" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/mj/imgWall" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/mj/notify" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/invite/hits" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/sd/imgWall" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/dall/imgWall" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/product/list" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/menu/list" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/markMap/client" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/doPay" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/payWays" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/detail" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/play" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/download" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/dall/models" ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/payment/notify/") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/static/") {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 统一参数处理
 | 
			
		||||
func parameterHandlerMiddleware() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		// GET 参数处理
 | 
			
		||||
		params := c.Request.URL.Query()
 | 
			
		||||
		for key, values := range params {
 | 
			
		||||
			for i, value := range values {
 | 
			
		||||
				params[key][i] = strings.TrimSpace(value)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// update get parameters
 | 
			
		||||
		c.Request.URL.RawQuery = params.Encode()
 | 
			
		||||
		// skip file upload requests
 | 
			
		||||
		contentType := c.Request.Header.Get("Content-Type")
 | 
			
		||||
		if strings.Contains(contentType, "multipart/form-data") {
 | 
			
		||||
			c.Next()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// WebSocket 连接请求验证
 | 
			
		||||
		if c.Request.URL.Path == "/api/chat" {
 | 
			
		||||
			sessionId := c.Query("sessionId")
 | 
			
		||||
			session := s.ChatSession.Get(sessionId)
 | 
			
		||||
			if session.ClientIP == c.ClientIP() {
 | 
			
		||||
		if strings.Contains(contentType, "application/json") {
 | 
			
		||||
			// process POST JSON request body
 | 
			
		||||
			bodyBytes, err := io.ReadAll(c.Request.Body)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				c.Next()
 | 
			
		||||
			} else {
 | 
			
		||||
				c.Abort()
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			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)))
 | 
			
		||||
		}
 | 
			
		||||
		session := sessions.Default(c)
 | 
			
		||||
		var value interface{}
 | 
			
		||||
		if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
 | 
			
		||||
			value = session.Get(types.SessionAdmin)
 | 
			
		||||
		} else {
 | 
			
		||||
			value = session.Get(types.SessionUser)
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if value != nil {
 | 
			
		||||
			c.Next()
 | 
			
		||||
		} else {
 | 
			
		||||
			resp.NotAuth(c)
 | 
			
		||||
			c.Abort()
 | 
			
		||||
	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,26 +21,16 @@ 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"},
 | 
			
		||||
		StaticDir:     "./static",
 | 
			
		||||
		StaticUrl:     "http://localhost/5678/static",
 | 
			
		||||
		Redis:         types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
 | 
			
		||||
		AesEncryptKey: utils.RandString(24),
 | 
			
		||||
		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{
 | 
			
		||||
			Driver:    types.SessionDriverCookie,
 | 
			
		||||
			SecretKey: utils.RandString(64),
 | 
			
		||||
			Name:      "CHAT_PLUS_SESSION",
 | 
			
		||||
			Domain:    "",
 | 
			
		||||
			Path:      "/",
 | 
			
		||||
			MaxAge:    86400,
 | 
			
		||||
			Secure:    true,
 | 
			
		||||
			HttpOnly:  false,
 | 
			
		||||
			SameSite:  http.SameSiteLaxMode,
 | 
			
		||||
		},
 | 
			
		||||
		ApiConfig: types.ChatPlusApiConfig{},
 | 
			
		||||
		ExtConfig: types.ChatPlusExtConfig{Token: utils.RandString(32)},
 | 
			
		||||
		ApiConfig: types.ApiConfig{},
 | 
			
		||||
		OSS: types.OSSConfig{
 | 
			
		||||
			Active: "local",
 | 
			
		||||
			Local: types.LocalStorageConfig{
 | 
			
		||||
@@ -42,6 +38,7 @@ func NewDefaultConfig() *types.AppConfig {
 | 
			
		||||
				BasePath: "./static/upload",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,28 @@
 | 
			
		||||
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"`
 | 
			
		||||
	Temperature float32       `json:"temperature"`
 | 
			
		||||
	MaxTokens   int           `json:"max_tokens"`
 | 
			
		||||
	Stream      bool          `json:"stream"`
 | 
			
		||||
	Messages    []interface{} `json:"messages"`
 | 
			
		||||
	Functions   []Function    `json:"functions"`
 | 
			
		||||
	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 {
 | 
			
		||||
@@ -26,29 +41,36 @@ type ChoiceItem struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Delta struct {
 | 
			
		||||
	Role         string       `json:"role"`
 | 
			
		||||
	Name         string       `json:"name"`
 | 
			
		||||
	Content      interface{}  `json:"content"`
 | 
			
		||||
	FunctionCall FunctionCall `json:"function_call,omitempty"`
 | 
			
		||||
	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 {
 | 
			
		||||
	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 模型
 | 
			
		||||
	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 MjTask struct {
 | 
			
		||||
	ChatId      string
 | 
			
		||||
	MessageId   string
 | 
			
		||||
	MessageHash string
 | 
			
		||||
	UserId      uint
 | 
			
		||||
	RoleId      uint
 | 
			
		||||
	Icon        string
 | 
			
		||||
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 {
 | 
			
		||||
@@ -62,13 +84,40 @@ type ApiError struct {
 | 
			
		||||
 | 
			
		||||
const PromptMsg = "prompt" // prompt message
 | 
			
		||||
const ReplyMsg = "reply"   // reply message
 | 
			
		||||
const MjMsg = "mj"
 | 
			
		||||
 | 
			
		||||
var ModelToTokens = map[string]int{
 | 
			
		||||
	"gpt-3.5-turbo":     4096,
 | 
			
		||||
	"gpt-3.5-turbo-16k": 16384,
 | 
			
		||||
	"gpt-4":             8192,
 | 
			
		||||
	"gpt-4-32k":         32768,
 | 
			
		||||
// 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 "兑换"
 | 
			
		||||
	case PowerGift:
 | 
			
		||||
		return "赠送"
 | 
			
		||||
	case PowerInvite:
 | 
			
		||||
		return "邀请"
 | 
			
		||||
	}
 | 
			
		||||
	return "其他"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TaskStorePrefix = "/tasks/"
 | 
			
		||||
type PowerMark int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	PowerSub = PowerMark(0)
 | 
			
		||||
	PowerAdd = PowerMark(1)
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,12 @@
 | 
			
		||||
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"
 | 
			
		||||
@@ -10,15 +17,17 @@ 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) *WsClient {
 | 
			
		||||
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,
 | 
			
		||||
@@ -36,6 +45,16 @@ func (wc *WsClient) Send(message []byte) error {
 | 
			
		||||
	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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,120 +1,173 @@
 | 
			
		||||
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"
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AppConfig struct {
 | 
			
		||||
	Path          string `toml:"-"`
 | 
			
		||||
	Listen        string
 | 
			
		||||
	Session       Session
 | 
			
		||||
	ProxyURL      string
 | 
			
		||||
	MysqlDns      string            // mysql 连接地址
 | 
			
		||||
	Manager       Manager           // 后台管理员账户信息
 | 
			
		||||
	StaticDir     string            // 静态资源目录
 | 
			
		||||
	StaticUrl     string            // 静态资源 URL
 | 
			
		||||
	Redis         RedisConfig       // redis 连接信息
 | 
			
		||||
	ApiConfig     ChatPlusApiConfig // ChatPlus API authorization configs
 | 
			
		||||
	AesEncryptKey string
 | 
			
		||||
	SmsConfig     AliYunSmsConfig   // AliYun send message service config
 | 
			
		||||
	ExtConfig     ChatPlusExtConfig // ChatPlus extensions callback api config
 | 
			
		||||
 | 
			
		||||
	OSS OSSConfig // OSS config
 | 
			
		||||
	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 ChatPlusApiConfig struct {
 | 
			
		||||
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 ChatPlusExtConfig struct {
 | 
			
		||||
	ApiURL 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 AliYunSmsConfig struct {
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Product      string
 | 
			
		||||
	Domain       string
 | 
			
		||||
type WechatPayConfig struct {
 | 
			
		||||
	Enabled    bool   // 是否启用该支付通道
 | 
			
		||||
	AppId      string // 公众号的APPID,如:wxd678efh567hg6787
 | 
			
		||||
	MchId      string // 直连商户的商户号,由微信支付生成并下发
 | 
			
		||||
	SerialNo   string // 商户证书的证书序列号
 | 
			
		||||
	PrivateKey string // 用户私钥文件路径
 | 
			
		||||
	ApiV3Key   string // API V3 秘钥
 | 
			
		||||
	NotifyURL  string // 异步通知地址
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OSSConfig struct {
 | 
			
		||||
	Active string
 | 
			
		||||
	Local  LocalStorageConfig
 | 
			
		||||
	Minio  MinioConfig
 | 
			
		||||
}
 | 
			
		||||
type MinioConfig struct {
 | 
			
		||||
	Endpoint     string
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Bucket       string
 | 
			
		||||
	UseSSL       bool
 | 
			
		||||
	Domain       string
 | 
			
		||||
type HuPiPayConfig struct { //虎皮椒第四方支付配置
 | 
			
		||||
	Enabled   bool   // 是否启用该支付通道
 | 
			
		||||
	AppId     string // App ID
 | 
			
		||||
	AppSecret string // app 密钥
 | 
			
		||||
	ApiURL    string // 支付网关
 | 
			
		||||
	NotifyURL string // 异步通知地址
 | 
			
		||||
	ReturnURL string // 同步通知地址
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LocalStorageConfig struct {
 | 
			
		||||
	BasePath string
 | 
			
		||||
	BaseURL  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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Manager 管理员
 | 
			
		||||
type Manager struct {
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SessionDriver string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	SessionDriverMem    = SessionDriver("mem")
 | 
			
		||||
	SessionDriverRedis  = SessionDriver("redis")
 | 
			
		||||
	SessionDriverCookie = SessionDriver("cookie")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Session configs struct
 | 
			
		||||
type Session struct {
 | 
			
		||||
	Driver    SessionDriver // session 存储驱动 mem|cookie|redis
 | 
			
		||||
	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"`
 | 
			
		||||
	ContextDeep   int     `json:"context_deep"` // 上下文深度
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SystemConfig struct {
 | 
			
		||||
	Title             string   `json:"title"`
 | 
			
		||||
	AdminTitle        string   `json:"admin_title"`
 | 
			
		||||
	Models            []string `json:"models"`
 | 
			
		||||
	UserInitCalls     int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用
 | 
			
		||||
	InitImgCalls      int      `json:"init_img_calls"`
 | 
			
		||||
	EnabledRegister   bool     `json:"enabled_register"`
 | 
			
		||||
	EnabledMsgService bool     `json:"enabled_msg_service"`
 | 
			
		||||
	Title         string `json:"title,omitempty"`           // 网站标题
 | 
			
		||||
	Slogan        string `json:"slogan,omitempty"`          // 网站 slogan
 | 
			
		||||
	AdminTitle    string `json:"admin_title,omitempty"`     // 管理后台标题
 | 
			
		||||
	Logo          string `json:"logo,omitempty"`            // 圆形 Logo
 | 
			
		||||
	BarLogo       string `json:"bar_logo,omitempty"`        // 条形 Logo
 | 
			
		||||
	InitPower     int    `json:"init_power,omitempty"`      // 新用户注册赠送算力值
 | 
			
		||||
	DailyPower    int    `json:"daily_power,omitempty"`     // 每日签到赠送算力
 | 
			
		||||
	InvitePower   int    `json:"invite_power,omitempty"`    // 邀请新用户赠送算力值
 | 
			
		||||
	VipMonthPower int    `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值
 | 
			
		||||
 | 
			
		||||
	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 生成视频消耗算力
 | 
			
		||||
	AdvanceVoicePower int `json:"advance_voice_power,omitempty"` // 高级语音对话消耗算力
 | 
			
		||||
	PromptPower       int `json:"prompt_power,omitempty"`        // 生成提示词消耗算力
 | 
			
		||||
 | 
			
		||||
	WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
 | 
			
		||||
 | 
			
		||||
	EnableContext bool `json:"enable_context,omitempty"`
 | 
			
		||||
	ContextDeep   int  `json:"context_deep,omitempty"`
 | 
			
		||||
 | 
			
		||||
	SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
 | 
			
		||||
	MjMode      string `json:"mj_mode"`       // midjourney 默认的API模式,relax, fast, turbo
 | 
			
		||||
 | 
			
		||||
	IndexNavs   []int  `json:"index_navs"`    // 首页显示的导航菜单
 | 
			
		||||
	Copyright   string `json:"copyright"`     // 版权信息
 | 
			
		||||
	ICP         string `json:"icp"`           // ICP 备案号
 | 
			
		||||
	MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
 | 
			
		||||
 | 
			
		||||
	EnabledVerify    bool     `json:"enabled_verify"`     // 是否启用验证码
 | 
			
		||||
	EmailWhiteList   []string `json:"email_white_list"`   // 邮箱白名单列表
 | 
			
		||||
	TranslateModelId int      `json:"translate_model_id"` // 用来做提示词翻译的大模型 id
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,100 +1,27 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
type FunctionCall struct {
 | 
			
		||||
	Name      string `json:"name"`
 | 
			
		||||
	Arguments string `json:"arguments"`
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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  Parameters `json:"parameters"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Parameters struct {
 | 
			
		||||
	Type       string              `json:"type"`
 | 
			
		||||
	Required   []string            `json:"required"`
 | 
			
		||||
	Properties map[string]Property `json:"properties"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Property struct {
 | 
			
		||||
	Type        string `json:"type"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	FuncZaoBao     = "zao_bao"     // 每日早报
 | 
			
		||||
	FuncHeadLine   = "headline"    // 今日头条
 | 
			
		||||
	FuncWeibo      = "weibo_hot"   // 微博热搜
 | 
			
		||||
	FuncMidJourney = "mid_journey" // MJ 绘画
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var InnerFunctions = []Function{
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncZaoBao,
 | 
			
		||||
		Description: "每日早报,获取当天全球的热门新闻事件列表",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncWeibo,
 | 
			
		||||
		Description: "新浪微博热搜榜,微博当日热搜榜单",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncHeadLine,
 | 
			
		||||
		Description: "今日头条,给用户推荐当天的头条新闻,周榜热文",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncMidJourney,
 | 
			
		||||
		Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"prompt": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "绘画内容描述,提示词,如果该参数中有中文的话,则需要翻译成英文",
 | 
			
		||||
				},
 | 
			
		||||
				"ar": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "图片长宽比,如 --ar 4:3",
 | 
			
		||||
				},
 | 
			
		||||
				"niji": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "动漫模型版本,例如 --niji 5",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	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 | context.CancelFunc | []interface{} | MjTask
 | 
			
		||||
	*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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,20 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
const SessionName = "ChatGPT-TOKEN"
 | 
			
		||||
const SessionUser = "SESSION_USER"        // 存储用户信息的 session key
 | 
			
		||||
const SessionAdmin = "SESSION_ADMIN"      //存储管理员信息的 session key
 | 
			
		||||
const LoginUserCache = "LOGIN_USER_CACHE" // 已登录用户缓存
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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 // 验证码短信模板 匹配
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										135
									
								
								api/core/types/task.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								api/core/types/task.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
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"`
 | 
			
		||||
	ModelId   uint   `json:"model_id"`
 | 
			
		||||
	ModelName string `json:"model_name"`
 | 
			
		||||
	Id       uint   `json:"id"`
 | 
			
		||||
	UserId   uint   `json:"user_id"`
 | 
			
		||||
	Prompt   string `json:"prompt"`
 | 
			
		||||
	N        int    `json:"n"`
 | 
			
		||||
	Quality  string `json:"quality"`
 | 
			
		||||
	Size     string `json:"size"`
 | 
			
		||||
	Style    string `json:"style"`
 | 
			
		||||
	Power     int    `json:"power"`
 | 
			
		||||
	TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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"`        // 视频时长(秒)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,12 @@
 | 
			
		||||
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"`
 | 
			
		||||
@@ -10,26 +17,54 @@ type BizVo struct {
 | 
			
		||||
	Data     interface{} `json:"data,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WsMessage Websocket message
 | 
			
		||||
type WsMessage struct {
 | 
			
		||||
	Type    WsMsgType   `json:"type"` // 消息类别,start, end, img
 | 
			
		||||
	Content interface{} `json:"content"`
 | 
			
		||||
// 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 (
 | 
			
		||||
	WsStart  = WsMsgType("start")
 | 
			
		||||
	WsMiddle = WsMsgType("middle")
 | 
			
		||||
	WsEnd    = WsMsgType("end")
 | 
			
		||||
	WsMjImg  = WsMsgType("mj")
 | 
			
		||||
	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(400) // 未授权
 | 
			
		||||
	NotAuthorized = BizCode(401) // 未授权
 | 
			
		||||
 | 
			
		||||
	OkMsg       = "Success"
 | 
			
		||||
	ErrorMsg    = "系统开小差了"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										82
									
								
								api/go.mod
									
									
									
									
									
								
							
							
						
						
									
										82
									
								
								api/go.mod
									
									
									
									
									
								
							@@ -1,36 +1,75 @@
 | 
			
		||||
module chatplus
 | 
			
		||||
module geekai
 | 
			
		||||
 | 
			
		||||
go 1.19
 | 
			
		||||
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/gin-contrib/sessions v0.0.5
 | 
			
		||||
	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/syndtr/goleveldb v1.0.0
 | 
			
		||||
	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/shirou/gopsutil v3.21.11+incompatible
 | 
			
		||||
	github.com/shopspring/decimal v1.3.1
 | 
			
		||||
	github.com/syndtr/goleveldb v1.0.0
 | 
			
		||||
	golang.org/x/image v0.15.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99 // indirect
 | 
			
		||||
	github.com/howeyc/fsnotify v0.9.0 // indirect
 | 
			
		||||
	github.com/mattn/go-colorable v0.1.13 // indirect
 | 
			
		||||
	github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a // indirect
 | 
			
		||||
	github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99 // 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/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // 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/golang/mock v1.6.0 // indirect
 | 
			
		||||
	github.com/gomodule/redigo v2.0.0+incompatible // 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
 | 
			
		||||
@@ -41,28 +80,27 @@ require (
 | 
			
		||||
	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/minio-go/v7 v7.0.62 // 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/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
 | 
			
		||||
	github.com/quic-go/qpack v0.4.0 // indirect
 | 
			
		||||
	github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
 | 
			
		||||
	github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
 | 
			
		||||
	github.com/quic-go/quic-go v0.35.1 // 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-20230522175609-2e198f4a06a1 // indirect
 | 
			
		||||
	golang.org/x/mod v0.11.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.14.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.12.0 // indirect
 | 
			
		||||
	golang.org/x/tools v0.10.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.30.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
 | 
			
		||||
)
 | 
			
		||||
@@ -72,10 +110,6 @@ require (
 | 
			
		||||
	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/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.4 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.19 // indirect
 | 
			
		||||
@@ -85,7 +119,7 @@ require (
 | 
			
		||||
	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.12.0
 | 
			
		||||
	golang.org/x/sys v0.11.0 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.23.0
 | 
			
		||||
	golang.org/x/sys v0.20.0 // indirect
 | 
			
		||||
	gorm.io/gorm v1.25.1
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										246
									
								
								api/go.sum
									
									
									
									
									
								
							
							
						
						
									
										246
									
								
								api/go.sum
									
									
									
									
									
								
							@@ -2,43 +2,77 @@ 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/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
 | 
			
		||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
 | 
			
		||||
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/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.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=
 | 
			
		||||
@@ -46,38 +80,35 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
 | 
			
		||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 | 
			
		||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 | 
			
		||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 | 
			
		||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 | 
			
		||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 | 
			
		||||
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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
			
		||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 | 
			
		||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
			
		||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
 | 
			
		||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 | 
			
		||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
 | 
			
		||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 | 
			
		||||
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/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.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
 | 
			
		||||
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/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/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99 h1:A6qlLfihaWef15viqtecCz4XknZcgjgD7mEuhu7bHEc=
 | 
			
		||||
github.com/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99/go.mod h1:ukFDwXV66bGV7JnfyxFKuKiVp4zH4orBKXML+VCSrhI=
 | 
			
		||||
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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 | 
			
		||||
github.com/howeyc/fsnotify v0.9.0 h1:0gtV5JmOKH4A8SsFxG2BczSeXWWPvcMT0euZt5gDAxY=
 | 
			
		||||
github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA=
 | 
			
		||||
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=
 | 
			
		||||
@@ -91,27 +122,32 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
 | 
			
		||||
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.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
 | 
			
		||||
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
 | 
			
		||||
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.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 | 
			
		||||
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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 | 
			
		||||
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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 | 
			
		||||
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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 | 
			
		||||
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=
 | 
			
		||||
@@ -124,39 +160,57 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 | 
			
		||||
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 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
 | 
			
		||||
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/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a h1:Tg4E4cXPZSZyd3H1tJlYo6ZreXV0ZJvE/lorNqyw1AU=
 | 
			
		||||
github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a/go.mod h1:9Or9aIl95Kp43zONcHd5tLZGKXb9iLx0pZjau0uJ5zg=
 | 
			
		||||
github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99 h1:+X7Gb40b5Bl3v5+3MiGK8Jhemjp65MHc+nkVCfq1Yfc=
 | 
			
		||||
github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99/go.mod h1:2LLTtftTZSdAPR/iVyennXZDLZOYzyDn+T0qEKJ8eSw=
 | 
			
		||||
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/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
 | 
			
		||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
 | 
			
		||||
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/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
 | 
			
		||||
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
 | 
			
		||||
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
 | 
			
		||||
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
 | 
			
		||||
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
 | 
			
		||||
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
 | 
			
		||||
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=
 | 
			
		||||
@@ -171,6 +225,10 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt
 | 
			
		||||
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=
 | 
			
		||||
@@ -179,7 +237,11 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
 | 
			
		||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 | 
			
		||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 | 
			
		||||
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=
 | 
			
		||||
@@ -188,6 +250,9 @@ 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=
 | 
			
		||||
@@ -196,66 +261,91 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
 | 
			
		||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
 | 
			
		||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
 | 
			
		||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
 | 
			
		||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 | 
			
		||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
 | 
			
		||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 | 
			
		||||
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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 | 
			
		||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
 | 
			
		||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
 | 
			
		||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
 | 
			
		||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 | 
			
		||||
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-20210220032951-036812b2e83c/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
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.0.0-20220811171246-fbc7d0a398ab/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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
 | 
			
		||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
 | 
			
		||||
golang.org/x/sys v0.11.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.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
 | 
			
		||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
			
		||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
 | 
			
		||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
			
		||||
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.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 | 
			
		||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
 | 
			
		||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
 | 
			
		||||
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=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 | 
			
		||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
 | 
			
		||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 | 
			
		||||
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 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
 | 
			
		||||
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=
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,27 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"strings"
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
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"
 | 
			
		||||
@@ -18,45 +29,105 @@ import (
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
const SuperManagerID = 1
 | 
			
		||||
 | 
			
		||||
type ManagerHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
	redis   *redis.Client
 | 
			
		||||
	captcha *service.CaptchaService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAdminHandler(app *core.AppServer, db *gorm.DB) *ManagerHandler {
 | 
			
		||||
	h := ManagerHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
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 types.Manager
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
	manager := h.App.Config.Manager
 | 
			
		||||
	if data.Username == manager.Username && data.Password == manager.Password {
 | 
			
		||||
		err := utils.SetLoginAdmin(c, manager)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "Save session failed")
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
		}
 | 
			
		||||
		manager.Password = "" // 清空密码]
 | 
			
		||||
		resp.SUCCESS(c, manager)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.ERROR(c, "用户名或者密码错误")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	session := sessions.Default(c)
 | 
			
		||||
	session.Delete(types.SessionAdmin)
 | 
			
		||||
	err := session.Save()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Save session failed")
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
@@ -64,75 +135,155 @@ func (h *ManagerHandler) Logout(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
// Session 会话检测
 | 
			
		||||
func (h *ManagerHandler) Session(c *gin.Context) {
 | 
			
		||||
	session := sessions.Default(c)
 | 
			
		||||
	admin := session.Get(types.SessionAdmin)
 | 
			
		||||
	if admin == nil {
 | 
			
		||||
	id := h.GetLoginUserId(c)
 | 
			
		||||
	key := fmt.Sprintf("admin/%d", id)
 | 
			
		||||
	if _, err := h.redis.Get(context.Background(), key).Result(); err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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":
 | 
			
		||||
		// 修改角色图片,改成绝对路径
 | 
			
		||||
		var roles []model.ChatRole
 | 
			
		||||
		h.db.Find(&roles)
 | 
			
		||||
		for _, r := range roles {
 | 
			
		||||
			if !strings.HasPrefix(r.Icon, "/") {
 | 
			
		||||
				r.Icon = "/" + r.Icon
 | 
			
		||||
				h.db.Updates(&r)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	case "history":
 | 
			
		||||
		// 修改角色图片,改成绝对路径
 | 
			
		||||
		var message []model.HistoryMessage
 | 
			
		||||
		h.db.Find(&message)
 | 
			
		||||
		for _, r := range message {
 | 
			
		||||
			if !strings.HasPrefix(r.Icon, "/") {
 | 
			
		||||
				r.Icon = "/" + r.Icon
 | 
			
		||||
				h.db.Updates(&r)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
 | 
			
		||||
	case "avatar":
 | 
			
		||||
		// 更新用户的头像地址
 | 
			
		||||
		var users []model.User
 | 
			
		||||
		h.db.Find(&users)
 | 
			
		||||
		for _, u := range users {
 | 
			
		||||
			if !strings.HasPrefix(u.Avatar, "/") {
 | 
			
		||||
				u.Avatar = "/" + u.Avatar
 | 
			
		||||
				h.db.Updates(&u)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	var manager model.AdminUser
 | 
			
		||||
	res := h.DB.Where("id", id).First(&manager)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, "SUCCESS")
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,22 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"time"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/handler"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
@@ -16,43 +24,47 @@ import (
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	return &ApiKeyHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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"`
 | 
			
		||||
		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{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)
 | 
			
		||||
	apiKey := model.ApiKey{}
 | 
			
		||||
	if data.Id > 0 {
 | 
			
		||||
		h.DB.Find(&apiKey, data.Id)
 | 
			
		||||
	}
 | 
			
		||||
	res := h.db.Save(&apiKey)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
	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)
 | 
			
		||||
	err = utils.CopyObject(apiKey, &keyVo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "数据拷贝失败!")
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("拷贝数据失败:%v", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	keyVo.Id = apiKey.Id
 | 
			
		||||
@@ -60,15 +72,23 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, keyVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List 获取 API KEY 列表
 | 
			
		||||
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)
 | 
			
		||||
	status := h.GetBool(c, "status")
 | 
			
		||||
	t := c.Query("type")
 | 
			
		||||
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	if status {
 | 
			
		||||
		session = session.Where("enabled", true)
 | 
			
		||||
	}
 | 
			
		||||
	if t != "" {
 | 
			
		||||
		types := strings.Split(t, "|")
 | 
			
		||||
		session = session.Where("type IN ?", types)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var items []model.ApiKey
 | 
			
		||||
	var keys = make([]vo.ApiKey, 0)
 | 
			
		||||
	res := query.Find(&items)
 | 
			
		||||
	res := session.Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var key vo.ApiKey
 | 
			
		||||
@@ -86,15 +106,37 @@ func (h *ApiKeyHandler) List(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, keys)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ApiKeyHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
func (h *ApiKeyHandler) Set(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id    uint        `json:"id"`
 | 
			
		||||
		Filed string      `json:"filed"`
 | 
			
		||||
		Value interface{} `json:"value"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if id > 0 {
 | 
			
		||||
		res := h.db.Where("id = ?", id).Delete(&model.ApiKey{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										194
									
								
								api/handler/admin/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								api/handler/admin/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,194 @@
 | 
			
		||||
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"`
 | 
			
		||||
		Type        string  `json:"type"`
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
	item.Type = data.Type
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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,49 +1,74 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"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
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
	levelDB        *store.LevelDB
 | 
			
		||||
	licenseService *service.LicenseService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
 | 
			
		||||
	h := ConfigHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
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 map[string]interface{} `json:"config"`
 | 
			
		||||
		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
 | 
			
		||||
	}
 | 
			
		||||
	str := utils.JsonEncode(&data.Config)
 | 
			
		||||
	config := model.Config{Key: data.Key, Config: str}
 | 
			
		||||
	res := h.db.FirstOrCreate(&config, model.Config{Key: data.Key})
 | 
			
		||||
 | 
			
		||||
	// 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 = str
 | 
			
		||||
		res := h.db.Updates(&config)
 | 
			
		||||
		config.Config = value
 | 
			
		||||
		res := h.DB.Updates(&config)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, res.Error.Error())
 | 
			
		||||
			return
 | 
			
		||||
@@ -51,12 +76,10 @@ func (h *ConfigHandler) Update(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
		// update config cache for AppServer
 | 
			
		||||
		var cfg model.Config
 | 
			
		||||
		h.db.Where("marker", data.Key).First(&cfg)
 | 
			
		||||
		h.DB.Where("marker", data.Key).First(&cfg)
 | 
			
		||||
		var err error
 | 
			
		||||
		if data.Key == "system" {
 | 
			
		||||
			err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
 | 
			
		||||
		} else if data.Key == "chat" {
 | 
			
		||||
			err = utils.JsonDecode(cfg.Config, &h.App.ChatConfig)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "Failed to update config cache: "+err.Error())
 | 
			
		||||
@@ -72,18 +95,115 @@ func (h *ConfigHandler) Update(c *gin.Context) {
 | 
			
		||||
func (h *ConfigHandler) Get(c *gin.Context) {
 | 
			
		||||
	key := c.Query("key")
 | 
			
		||||
	var config model.Config
 | 
			
		||||
	res := h.db.Where("marker", key).First(&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)
 | 
			
		||||
	var value map[string]interface{}
 | 
			
		||||
	err := utils.JsonDecode(config.Config, &value)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, m)
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,38 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"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
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
 | 
			
		||||
	h := DashboardHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
	return &DashboardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type statsVo struct {
 | 
			
		||||
	Users   int64   `json:"users"`
 | 
			
		||||
	Chats   int64   `json:"chats"`
 | 
			
		||||
	Tokens  int64   `json:"tokens"`
 | 
			
		||||
	Rewards float64 `json:"rewards"`
 | 
			
		||||
	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) {
 | 
			
		||||
@@ -34,30 +41,70 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
 | 
			
		||||
	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)
 | 
			
		||||
	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)
 | 
			
		||||
	res = h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Chats = chatCount
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// tokens took stats
 | 
			
		||||
	var tokenCount int64
 | 
			
		||||
	res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Tokens = tokenCount
 | 
			
		||||
	var historyMessages []model.ChatMessage
 | 
			
		||||
	res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages)
 | 
			
		||||
	for _, item := range historyMessages {
 | 
			
		||||
		stats.Tokens += item.Tokens
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// reward revenue
 | 
			
		||||
	var amount float64
 | 
			
		||||
	res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Rewards = amount
 | 
			
		||||
	// 订单收入
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								api/handler/admin/power_log_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								api/handler/admin/power_log_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
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"`
 | 
			
		||||
		UserId   uint     `json:"userid"`
 | 
			
		||||
		Type     int      `json:"type"`
 | 
			
		||||
		Model    string   `json:"model"`
 | 
			
		||||
		Date     []string `json:"date"`
 | 
			
		||||
		Page     int      `json:"page"`
 | 
			
		||||
		PageSize int      `json:"page_size"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	if data.Model != "" {
 | 
			
		||||
		session = session.Where("model", data.Model)
 | 
			
		||||
	}
 | 
			
		||||
	if data.Type > 0 {
 | 
			
		||||
		session = session.Where("type", data.Type)
 | 
			
		||||
	}
 | 
			
		||||
	if data.UserId > 0 {
 | 
			
		||||
		session = session.Where("user_id", data.UserId)
 | 
			
		||||
	}
 | 
			
		||||
	if data.Username != "" {
 | 
			
		||||
		session = session.Where("username", data.Username)
 | 
			
		||||
	}
 | 
			
		||||
	if len(data.Date) == 2 {
 | 
			
		||||
		start := data.Date[0] + " 00:00:00"
 | 
			
		||||
		end := data.Date[1] + " 00:00:00"
 | 
			
		||||
		session = session.Where("created_at >= ? AND created_at <= ?", start, end)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var total int64
 | 
			
		||||
	session.Model(&model.PowerLog{}).Count(&total)
 | 
			
		||||
	var items []model.PowerLog
 | 
			
		||||
	var list = make([]vo.PowerLog, 0)
 | 
			
		||||
	offset := (data.Page - 1) * data.PageSize
 | 
			
		||||
	res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var log vo.PowerLog
 | 
			
		||||
			err := utils.CopyObject(item, &log)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			log.Id = item.Id
 | 
			
		||||
			log.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
			log.TypeStr = item.Type.String()
 | 
			
		||||
			list = append(list, log)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 统计消费算力总和
 | 
			
		||||
	var totalPower float64
 | 
			
		||||
	if len(data.Date) == 2 {
 | 
			
		||||
		session.Where("mark", 0).Select("SUM(amount) as total_sum").Scan(&totalPower)
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, gin.H{"data": vo.NewPage(total, data.Page, data.PageSize, list), "stat": totalPower})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RewardHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
 | 
			
		||||
	h := RewardHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *RewardHandler) List(c *gin.Context) {
 | 
			
		||||
	var items []model.Reward
 | 
			
		||||
	res := h.db.Find(&items)
 | 
			
		||||
	var rewards = make([]vo.Reward, 0)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		userIds := make([]uint, 0)
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			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 items {
 | 
			
		||||
			var r vo.Reward
 | 
			
		||||
			err := utils.CopyObject(v, &r)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			r.Id = v.Id
 | 
			
		||||
			r.Username = userMap[v.UserId].Username
 | 
			
		||||
			r.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
			r.UpdatedAt = v.UpdatedAt.Unix()
 | 
			
		||||
			rewards = append(rewards, r)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, rewards)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,14 +1,25 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/handler"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
@@ -16,37 +27,40 @@ import (
 | 
			
		||||
 | 
			
		||||
type UserHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
	licenseService *service.LicenseService
 | 
			
		||||
	redis          *redis.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
 | 
			
		||||
	h := UserHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
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)
 | 
			
		||||
	mobile := h.GetTrim(c, "mobile")
 | 
			
		||||
	username := h.GetTrim(c, "username")
 | 
			
		||||
	mobile := h.GetTrim(c, "mobile")
 | 
			
		||||
	email := h.GetTrim(c, "email")
 | 
			
		||||
 | 
			
		||||
	offset := (page - 1) * pageSize
 | 
			
		||||
	var items []model.User
 | 
			
		||||
	var users = make([]vo.User, 0)
 | 
			
		||||
	var total int64
 | 
			
		||||
 | 
			
		||||
	session := h.db.Session(&gorm.Session{})
 | 
			
		||||
	if mobile != "" {
 | 
			
		||||
		session = session.Where("mobile LIKE ?", "%"+mobile+"%")
 | 
			
		||||
	}
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	if username != "" {
 | 
			
		||||
		session = session.Where("username LIKE ?", "%"+username+"%")
 | 
			
		||||
	}
 | 
			
		||||
	if mobile != "" {
 | 
			
		||||
		session = session.Where("mobile LIKE ?", "%"+mobile+"%")
 | 
			
		||||
	}
 | 
			
		||||
	if email != "" {
 | 
			
		||||
		session = session.Where("email LIKE ?", "%"+email+"%")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session.Model(&model.User{}).Count(&total)
 | 
			
		||||
	res := session.Offset(offset).Limit(pageSize).Find(&items)
 | 
			
		||||
	res := session.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var user vo.User
 | 
			
		||||
@@ -68,58 +82,110 @@ func (h *UserHandler) List(c *gin.Context) {
 | 
			
		||||
func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id          uint     `json:"id"`
 | 
			
		||||
		Username    string   `json:"username"`
 | 
			
		||||
		Password    string   `json:"password"`
 | 
			
		||||
		Username    string   `json:"username"`
 | 
			
		||||
		Mobile      string   `json:"mobile"`
 | 
			
		||||
		Nickname    string   `json:"nickname"`
 | 
			
		||||
		Calls       int      `json:"calls"`
 | 
			
		||||
		ImgCalls    int      `json:"img_calls"`
 | 
			
		||||
		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 { // 更新
 | 
			
		||||
		user.Id = data.Id
 | 
			
		||||
		// 此处需要用 map 更新,用结构体无法更新 0 值
 | 
			
		||||
		res = h.db.Model(&user).Updates(map[string]interface{}{
 | 
			
		||||
			"nickname":        data.Nickname,
 | 
			
		||||
			"mobile":          data.Mobile,
 | 
			
		||||
			"calls":           data.Calls,
 | 
			
		||||
			"img_calls":       data.ImgCalls,
 | 
			
		||||
			"status":          data.Status,
 | 
			
		||||
			"chat_roles_json": utils.JsonEncode(data.ChatRoles),
 | 
			
		||||
			"expired_time":    utils.Str2stamp(data.ExpiredTime),
 | 
			
		||||
		})
 | 
			
		||||
		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),
 | 
			
		||||
			Nickname:    fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)),
 | 
			
		||||
			Mobile:      data.Mobile,
 | 
			
		||||
			Email:       data.Email,
 | 
			
		||||
			Avatar:      "/images/avatar/user.png",
 | 
			
		||||
			Salt:        salt,
 | 
			
		||||
			Power:       data.Power,
 | 
			
		||||
			Status:      true,
 | 
			
		||||
			Mobile:      data.Mobile,
 | 
			
		||||
			ChatRoles:   utils.JsonEncode(data.ChatRoles),
 | 
			
		||||
			ChatModels:  utils.JsonEncode(data.ChatModels),
 | 
			
		||||
			ExpiredTime: utils.Str2stamp(data.ExpiredTime),
 | 
			
		||||
			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:        "",
 | 
			
		||||
			}),
 | 
			
		||||
			Calls: h.App.SysConfig.UserInitCalls,
 | 
			
		||||
		}
 | 
			
		||||
		res = h.db.Create(&u)
 | 
			
		||||
		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()
 | 
			
		||||
@@ -127,7 +193,7 @@ func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败")
 | 
			
		||||
		resp.ERROR(c, res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -146,7 +212,7 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res := h.db.First(&user, data.Id)
 | 
			
		||||
	res := h.DB.First(&user, data.Id)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "No user found")
 | 
			
		||||
		return
 | 
			
		||||
@@ -154,7 +220,7 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	password := utils.GenPassword(data.Password, user.Salt)
 | 
			
		||||
	user.Password = password
 | 
			
		||||
	res = h.db.Updates(&user)
 | 
			
		||||
	res = h.DB.Updates(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c)
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -163,37 +229,69 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	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
 | 
			
		||||
		}
 | 
			
		||||
		// 删除聊天记录
 | 
			
		||||
		res = h.db.Where("user_id = ?", id).Delete(&model.ChatItem{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			resp.ERROR(c, "删除失败")
 | 
			
		||||
			return
 | 
			
		||||
		if err = tx.Unscoped().Where("user_id = ?", id).Delete(&model.ChatItem{}).Error; err != nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		// 删除聊天历史记录
 | 
			
		||||
		res = h.db.Where("user_id = ?", id).Delete(&model.HistoryMessage{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			resp.ERROR(c, "删除失败")
 | 
			
		||||
			return
 | 
			
		||||
		if err = tx.Unscoped().Where("user_id = ?", id).Delete(&model.ChatMessage{}).Error; err != nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		// 删除登录日志
 | 
			
		||||
		res = h.db.Where("user_id = ?", id).Delete(&model.UserLoginLog{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			resp.ERROR(c, "删除失败")
 | 
			
		||||
			return
 | 
			
		||||
		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
 | 
			
		||||
		}
 | 
			
		||||
		tx.Commit()
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	tx.Commit()
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -201,10 +299,10 @@ 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)
 | 
			
		||||
	h.DB.Model(&model.UserLoginLog{}).Count(&total)
 | 
			
		||||
	offset := (page - 1) * pageSize
 | 
			
		||||
	var items []model.UserLoginLog
 | 
			
		||||
	res := h.db.Offset(offset).Limit(pageSize).Find(&items)
 | 
			
		||||
	res := h.DB.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "获取数据失败")
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,21 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	logger2 "geekai/logger"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
@@ -13,6 +25,7 @@ var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
type BaseHandler struct {
 | 
			
		||||
	App *core.AppServer
 | 
			
		||||
	DB  *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) GetTrim(c *gin.Context, key string) string {
 | 
			
		||||
@@ -20,47 +33,62 @@ func (h *BaseHandler) GetTrim(c *gin.Context, key string) string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int {
 | 
			
		||||
	return intValue(c.PostForm(key), defaultValue)
 | 
			
		||||
	return utils.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
 | 
			
		||||
	return utils.IntValue(c.Query(key), defaultValue)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 {
 | 
			
		||||
	return floatValue(c.Query(key))
 | 
			
		||||
	return utils.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
 | 
			
		||||
	return utils.FloatValue(c.PostForm(key))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
 | 
			
		||||
	return boolValue(c.Query(key))
 | 
			
		||||
	return utils.BoolValue(c.Query(key))
 | 
			
		||||
}
 | 
			
		||||
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
 | 
			
		||||
	return boolValue(c.PostForm(key))
 | 
			
		||||
	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 boolValue(str string) bool {
 | 
			
		||||
	value, err := strconv.ParseBool(str)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
 | 
			
		||||
	userId, ok := c.Get(types.LoginUserID)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return value
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,16 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -45,3 +52,33 @@ func (h *CaptchaHandler) Check(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,147 +1,74 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"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/gorilla/websocket"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
 | 
			
		||||
 | 
			
		||||
type ChatHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db      *gorm.DB
 | 
			
		||||
	leveldb *store.LevelDB
 | 
			
		||||
	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, levelDB *store.LevelDB) *ChatHandler {
 | 
			
		||||
	handler := ChatHandler{db: db, leveldb: levelDB}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return &handler
 | 
			
		||||
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,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var chatConfig types.ChatConfig
 | 
			
		||||
 | 
			
		||||
// 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 == nil {
 | 
			
		||||
		user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Info("用户未登录")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		session = &types.ChatSession{
 | 
			
		||||
			SessionId: sessionId,
 | 
			
		||||
			ClientIP:  c.ClientIP(),
 | 
			
		||||
			Username:  user.Username,
 | 
			
		||||
			UserId:    user.Id,
 | 
			
		||||
		}
 | 
			
		||||
		h.App.ChatSession.Put(sessionId, session)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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 {
 | 
			
		||||
		utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 初始化聊天配置
 | 
			
		||||
	var config model.Config
 | 
			
		||||
	h.db.Where("marker", "chat").First(&config)
 | 
			
		||||
	err = utils.JsonDecode(config.Config, &chatConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 保存会话连接
 | 
			
		||||
	h.App.ChatClients.Put(sessionId, client)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			_, msg, err := client.Receive()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
				client.Close()
 | 
			
		||||
				h.App.ChatClients.Delete(sessionId)
 | 
			
		||||
				h.App.ReqCancelFunc.Delete(sessionId)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			message := string(msg)
 | 
			
		||||
			logger.Info("Receive a message: ", message)
 | 
			
		||||
			//utils.ReplyMessage(client, "这是一条测试消息!")
 | 
			
		||||
			ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
			h.App.ReqCancelFunc.Put(sessionId, cancel)
 | 
			
		||||
			// 回复消息
 | 
			
		||||
			err = h.sendMessage(ctx, session, chatRole, message, client)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
			} else {
 | 
			
		||||
				utils.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.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	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)
 | 
			
		||||
	res := h.DB.Model(&model.User{}).First(&user, session.UserId)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		utils.ReplyMessage(ws, "非法用户,请联系管理员!")
 | 
			
		||||
		return res.Error
 | 
			
		||||
		return errors.New("未授权用户,您正在进行非法操作!")
 | 
			
		||||
	}
 | 
			
		||||
	var userVo vo.User
 | 
			
		||||
	err := utils.CopyObject(user, &userVo)
 | 
			
		||||
@@ -151,67 +78,84 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if userVo.Status == false {
 | 
			
		||||
		utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return nil
 | 
			
		||||
		return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if userVo.Calls <= 0 && userVo.ChatConfig.ApiKey == "" {
 | 
			
		||||
		utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!")
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return nil
 | 
			
		||||
	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() {
 | 
			
		||||
		utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return nil
 | 
			
		||||
		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,
 | 
			
		||||
		Temperature: userVo.ChatConfig.Temperature,
 | 
			
		||||
		MaxTokens:   userVo.ChatConfig.MaxTokens,
 | 
			
		||||
		Stream:      true,
 | 
			
		||||
		Functions:   types.InnerFunctions,
 | 
			
		||||
		Model: session.Model.Value,
 | 
			
		||||
	}
 | 
			
		||||
	// 兼容 GPT-O1 模型
 | 
			
		||||
	if strings.HasPrefix(session.Model.Value, "o1-") {
 | 
			
		||||
		utils.SendChunkMsg(ws, "> AI 正在思考...\n")
 | 
			
		||||
		req.Stream = session.Stream
 | 
			
		||||
		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"
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 加载聊天上下文
 | 
			
		||||
	var chatCtx []interface{}
 | 
			
		||||
	if userVo.ChatConfig.EnableContext {
 | 
			
		||||
		if h.App.ChatContexts.Has(session.ChatId) {
 | 
			
		||||
			chatCtx = h.App.ChatContexts.Get(session.ChatId)
 | 
			
		||||
	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 {
 | 
			
		||||
			// calculate the tokens of current request, to prevent to exceeding the max tokens num
 | 
			
		||||
			tokens := req.MaxTokens
 | 
			
		||||
			for _, f := range types.InnerFunctions {
 | 
			
		||||
				tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model)
 | 
			
		||||
				tokens += tks
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// loading the role context
 | 
			
		||||
			var messages []types.Message
 | 
			
		||||
			err := utils.JsonDecode(role.Context, &messages)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				for _, v := range messages {
 | 
			
		||||
					tks, _ := utils.CalcTokens(v.Content, req.Model)
 | 
			
		||||
					if tokens+tks >= types.ModelToTokens[req.Model] {
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
					tokens += tks
 | 
			
		||||
					chatCtx = append(chatCtx, v)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// loading recent chat history as chat context
 | 
			
		||||
			if chatConfig.ContextDeep > 0 {
 | 
			
		||||
				var historyMessages []model.HistoryMessage
 | 
			
		||||
				res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("created_at desc").Find(&historyMessages)
 | 
			
		||||
			_ = 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 _, msg := range historyMessages {
 | 
			
		||||
						if tokens+msg.Tokens >= types.ModelToTokens[session.Model] {
 | 
			
		||||
							break
 | 
			
		||||
						}
 | 
			
		||||
						tokens += msg.Tokens
 | 
			
		||||
					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"
 | 
			
		||||
@@ -221,352 +165,129 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 计算当前请求的 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 _, m := range chatCtx {
 | 
			
		||||
		reqMgs = append(reqMgs, m)
 | 
			
		||||
 | 
			
		||||
	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": prompt,
 | 
			
		||||
		"content": content,
 | 
			
		||||
	})
 | 
			
		||||
	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 if strings.Contains(err.Error(), "no available key") {
 | 
			
		||||
			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY🔑,您可以导入自己的 API KEY🔑 继续使用!🙏🙏🙏")
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			logger.Error(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		utils.ReplyMessage(ws, ErrorMsg)
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		defer response.Body.Close()
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debugf("%+v", req.Messages)
 | 
			
		||||
 | 
			
		||||
	contentType := response.Header.Get("Content-Type")
 | 
			
		||||
	if strings.Contains(contentType, "text/event-stream") {
 | 
			
		||||
		if true {
 | 
			
		||||
			replyCreatedAt := time.Now()
 | 
			
		||||
			// 循环读取 Chunk 消息
 | 
			
		||||
			var message = types.Message{}
 | 
			
		||||
			var contents = make([]string, 0)
 | 
			
		||||
			var functionCall = false
 | 
			
		||||
			var functionName string
 | 
			
		||||
			var arguments = make([]string, 0)
 | 
			
		||||
			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 if err != io.EOF {
 | 
			
		||||
						logger.Error("信息读取出错:", err)
 | 
			
		||||
					}
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
				if !strings.Contains(line, "data:") || len(line) < 30 {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				var responseBody = types.ApiResponse{}
 | 
			
		||||
				err = json.Unmarshal([]byte(line[6:]), &responseBody)
 | 
			
		||||
				if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
 | 
			
		||||
					logger.Error(err, line)
 | 
			
		||||
					utils.ReplyMessage(ws, ErrorMsg)
 | 
			
		||||
					utils.ReplyMessage(ws, "")
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				fun := responseBody.Choices[0].Delta.FunctionCall
 | 
			
		||||
				if functionCall && fun.Name == "" {
 | 
			
		||||
					arguments = append(arguments, fun.Arguments)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if !utils.IsEmptyValue(fun) {
 | 
			
		||||
					functionCall = true
 | 
			
		||||
					functionName = fun.Name
 | 
			
		||||
					f := h.App.Functions[functionName]
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 初始化 role
 | 
			
		||||
				if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
 | 
			
		||||
					message.Role = responseBody.Choices[0].Delta.Role
 | 
			
		||||
					utils.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, utils.InterfaceToString(content))
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
						Type:    types.WsMiddle,
 | 
			
		||||
						Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
 | 
			
		||||
					})
 | 
			
		||||
				}
 | 
			
		||||
			} // end for
 | 
			
		||||
 | 
			
		||||
			if functionCall { // 调用函数完成任务
 | 
			
		||||
				var params map[string]interface{}
 | 
			
		||||
				_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
 | 
			
		||||
				logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
 | 
			
		||||
 | 
			
		||||
				// for creating image, check if the user's img_calls > 0
 | 
			
		||||
				if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
 | 
			
		||||
					utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
 | 
			
		||||
					utils.ReplyMessage(ws, "")
 | 
			
		||||
				} else {
 | 
			
		||||
					f := h.App.Functions[functionName]
 | 
			
		||||
					data, err := f.Invoke(params)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						msg := "调用函数出错:" + err.Error()
 | 
			
		||||
						utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
							Type:    types.WsMiddle,
 | 
			
		||||
							Content: msg,
 | 
			
		||||
						})
 | 
			
		||||
						contents = append(contents, msg)
 | 
			
		||||
					} else {
 | 
			
		||||
						content := data
 | 
			
		||||
						if functionName == types.FuncMidJourney {
 | 
			
		||||
							key := utils.Sha256(data)
 | 
			
		||||
							logger.Debug(data, ",", key)
 | 
			
		||||
							// add task for MidJourney
 | 
			
		||||
							h.App.MjTaskClients.Put(key, ws)
 | 
			
		||||
							task := types.MjTask{
 | 
			
		||||
								UserId: userVo.Id,
 | 
			
		||||
								RoleId: role.Id,
 | 
			
		||||
								Icon:   "/images/avatar/mid_journey.png",
 | 
			
		||||
								ChatId: session.ChatId,
 | 
			
		||||
							}
 | 
			
		||||
							err := h.leveldb.Put(types.TaskStorePrefix+key, task)
 | 
			
		||||
							if err != nil {
 | 
			
		||||
								logger.Error("error with store MidJourney task: ", err)
 | 
			
		||||
							}
 | 
			
		||||
							content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
 | 
			
		||||
 | 
			
		||||
							// update user's img_calls
 | 
			
		||||
							h.db.Model(&user).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
							Type:    types.WsMiddle,
 | 
			
		||||
							Content: content,
 | 
			
		||||
						})
 | 
			
		||||
						contents = append(contents, content)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 消息发送成功
 | 
			
		||||
			if len(contents) > 0 {
 | 
			
		||||
				// 更新用户的对话次数
 | 
			
		||||
				if userVo.ChatConfig.ApiKey == "" { // 如果用户使用的是自己绑定的 API KEY 则不扣减对话次数
 | 
			
		||||
					h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if message.Role == "" {
 | 
			
		||||
					message.Role = "assistant"
 | 
			
		||||
				}
 | 
			
		||||
				message.Content = strings.Join(contents, "")
 | 
			
		||||
				useMsg := types.Message{Role: "user", Content: prompt}
 | 
			
		||||
 | 
			
		||||
				// 更新上下文消息,如果是调用函数则不需要更新上下文
 | 
			
		||||
				if userVo.ChatConfig.EnableContext && functionCall == false {
 | 
			
		||||
					chatCtx = append(chatCtx, useMsg)  // 提问消息
 | 
			
		||||
					chatCtx = append(chatCtx, message) // 回复消息
 | 
			
		||||
					h.App.ChatContexts.Put(session.ChatId, chatCtx)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 追加聊天记录
 | 
			
		||||
				if userVo.ChatConfig.EnableHistory {
 | 
			
		||||
					useContext := true
 | 
			
		||||
					if functionCall {
 | 
			
		||||
						useContext = false
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// for prompt
 | 
			
		||||
					promptToken, 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:     promptToken,
 | 
			
		||||
						UseContext: useContext,
 | 
			
		||||
					}
 | 
			
		||||
					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 数量
 | 
			
		||||
					var replyToken = 0
 | 
			
		||||
					if functionCall { // 函数名 + 参数 token
 | 
			
		||||
						tokens, _ := utils.CalcTokens(functionName, req.Model)
 | 
			
		||||
						replyToken += tokens
 | 
			
		||||
						tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
 | 
			
		||||
						replyToken += tokens
 | 
			
		||||
					} else {
 | 
			
		||||
						replyToken, _ = utils.CalcTokens(message.Content, req.Model)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					historyReplyMsg := model.HistoryMessage{
 | 
			
		||||
						UserId:     userVo.Id,
 | 
			
		||||
						ChatId:     session.ChatId,
 | 
			
		||||
						RoleId:     role.Id,
 | 
			
		||||
						Type:       types.ReplyMsg,
 | 
			
		||||
						Icon:       role.Icon,
 | 
			
		||||
						Content:    message.Content,
 | 
			
		||||
						Tokens:     replyToken,
 | 
			
		||||
						UseContext: useContext,
 | 
			
		||||
					}
 | 
			
		||||
					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)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// 计算本次对话消耗的总 token 数量
 | 
			
		||||
					var totalTokens = 0
 | 
			
		||||
					if functionCall { // prompt + 函数名 + 参数 token
 | 
			
		||||
						totalTokens = promptToken + replyToken
 | 
			
		||||
					} else {
 | 
			
		||||
						totalTokens = replyToken + getTotalTokens(req)
 | 
			
		||||
					}
 | 
			
		||||
					//utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("\n\n `本轮对话共消耗 Token 数量: %d`", totalTokens+11)})
 | 
			
		||||
					if userVo.ChatConfig.ApiKey != "" { // 调用自己的 API KEY 不计算 token 消耗
 | 
			
		||||
						h.db.Model(&user).UpdateColumn("tokens", gorm.Expr("tokens + ?",
 | 
			
		||||
							totalTokens))
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 保存当前会话
 | 
			
		||||
				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") {
 | 
			
		||||
			utils.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") {
 | 
			
		||||
			utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。")
 | 
			
		||||
		} else if strings.Contains(res.Error.Message, "This model's maximum context length") {
 | 
			
		||||
			logger.Error(res.Error.Message)
 | 
			
		||||
			utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
 | 
			
		||||
			h.App.ChatContexts.Delete(session.ChatId)
 | 
			
		||||
			return h.sendMessage(ctx, session, role, prompt, ws)
 | 
			
		||||
		} else {
 | 
			
		||||
			utils.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.Config.ProxyURL
 | 
			
		||||
	if proxyURL == "" {
 | 
			
		||||
		client = &http.Client{}
 | 
			
		||||
	} else { // 使用代理
 | 
			
		||||
		proxy, _ := url.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)
 | 
			
		||||
	return h.sendOpenAiMessage(req, userVo, ctx, session, role, prompt, ws)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tokens 统计 token 数量
 | 
			
		||||
func (h *ChatHandler) Tokens(c *gin.Context) {
 | 
			
		||||
	text := c.Query("text")
 | 
			
		||||
	md := c.Query("model")
 | 
			
		||||
	tokens, err := utils.CalcTokens(text, md)
 | 
			
		||||
	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
 | 
			
		||||
@@ -598,9 +319,203 @@ func getTotalTokens(req types.ApiRequest) int {
 | 
			
		||||
// 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)
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,108 +0,0 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chatIds = make([]string, 0)
 | 
			
		||||
	for _, chat := range chats {
 | 
			
		||||
		chatIds = append(chatIds, chat.ChatId)
 | 
			
		||||
		// 清空会话上下文
 | 
			
		||||
		h.App.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.HistoryMessage{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			return res.Error
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
 | 
			
		||||
		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)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,52 +1,158 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"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) {
 | 
			
		||||
	userId := h.GetInt(c, "user_id", 0)
 | 
			
		||||
	if userId == 0 {
 | 
			
		||||
		resp.ERROR(c, "The parameter 'user_id' is needed.")
 | 
			
		||||
	if !h.IsLogin(c) {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	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
 | 
			
		||||
			}
 | 
			
		||||
	h.DB.Where("user_id", userId).Order("id DESC").Find(&chats)
 | 
			
		||||
	if len(chats) == 0 {
 | 
			
		||||
		resp.SUCCESS(c, items)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
			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)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	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")
 | 
			
		||||
@@ -54,20 +160,20 @@ func (h *ChatHandler) Remove(c *gin.Context) {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	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{})
 | 
			
		||||
	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{})
 | 
			
		||||
	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
 | 
			
		||||
@@ -76,10 +182,11 @@ func (h *ChatHandler) Remove(c *gin.Context) {
 | 
			
		||||
	// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
 | 
			
		||||
 | 
			
		||||
	// 清空会话上下文
 | 
			
		||||
	h.App.ChatContexts.Delete(chatId)
 | 
			
		||||
	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) {
 | 
			
		||||
@@ -88,18 +195,26 @@ func (h *ChatHandler) Detail(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chatItem model.ChatItem
 | 
			
		||||
	res := h.db.Where("chat_id = ?", chatId).First(&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("type", "chat").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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										241
									
								
								api/handler/chat_openai_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								api/handler/chat_openai_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,241 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	req2 "github.com/imroc/req/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Usage struct {
 | 
			
		||||
	Prompt           string `json:"prompt,omitempty"`
 | 
			
		||||
	Content          string `json:"content,omitempty"`
 | 
			
		||||
	PromptTokens     int    `json:"prompt_tokens"`
 | 
			
		||||
	CompletionTokens int    `json:"completion_tokens"`
 | 
			
		||||
	TotalTokens      int    `json:"total_tokens"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAIResVo struct {
 | 
			
		||||
	Id                string `json:"id"`
 | 
			
		||||
	Object            string `json:"object"`
 | 
			
		||||
	Created           int    `json:"created"`
 | 
			
		||||
	Model             string `json:"model"`
 | 
			
		||||
	SystemFingerprint string `json:"system_fingerprint"`
 | 
			
		||||
	Choices           []struct {
 | 
			
		||||
		Index   int `json:"index"`
 | 
			
		||||
		Message struct {
 | 
			
		||||
			Role    string `json:"role"`
 | 
			
		||||
			Content string `json:"content"`
 | 
			
		||||
		} `json:"message"`
 | 
			
		||||
		Logprobs     interface{} `json:"logprobs"`
 | 
			
		||||
		FinishReason string      `json:"finish_reason"`
 | 
			
		||||
	} `json:"choices"`
 | 
			
		||||
	Usage Usage `json:"usage"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OPenAI 消息发送实现
 | 
			
		||||
func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
	req types.ApiRequest,
 | 
			
		||||
	userVo vo.User,
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	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.Since(start))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if strings.Contains(err.Error(), "context canceled") {
 | 
			
		||||
			return fmt.Errorf("用户取消了请求:%s", prompt)
 | 
			
		||||
		} else if strings.Contains(err.Error(), "no available key") {
 | 
			
		||||
			return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		defer response.Body.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if response.StatusCode != 200 {
 | 
			
		||||
		body, _ := io.ReadAll(response.Body)
 | 
			
		||||
		return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contentType := response.Header.Get("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)
 | 
			
		||||
 | 
			
		||||
		if strings.HasPrefix(req.Model, "o1-") {
 | 
			
		||||
			content := fmt.Sprintf("AI 思考结束,耗时:%d 秒。\n\n", time.Now().Unix()-session.Start)
 | 
			
		||||
			contents = append(contents, "> AI 正在思考中...\n")
 | 
			
		||||
			contents = append(contents, content)
 | 
			
		||||
			utils.SendChunkMsg(ws, content)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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, 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).Post(function.Action)
 | 
			
		||||
			errMsg := ""
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				errMsg = err.Error()
 | 
			
		||||
			} else {
 | 
			
		||||
				all, _ := io.ReadAll(r.Body)
 | 
			
		||||
				err = json.Unmarshal(all, &apiRes)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					errMsg = err.Error()
 | 
			
		||||
				} else if apiRes.Code != types.Success {
 | 
			
		||||
					errMsg = apiRes.Message
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if errMsg != "" {
 | 
			
		||||
				errMsg = "调用函数工具出错:" + errMsg
 | 
			
		||||
				contents = append(contents, errMsg)
 | 
			
		||||
			} else {
 | 
			
		||||
				errMsg = utils.InterfaceToString(apiRes.Data)
 | 
			
		||||
				contents = append(contents, errMsg)
 | 
			
		||||
			}
 | 
			
		||||
			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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,19 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
@@ -13,41 +21,28 @@ import (
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	return &ChatRoleHandler{BaseHandler: BaseHandler{App: app, DB: db}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List get user list
 | 
			
		||||
// List 获取用户聊天应用列表
 | 
			
		||||
func (h *ChatRoleHandler) List(c *gin.Context) {
 | 
			
		||||
	tid := h.GetInt(c, "tid", 0)
 | 
			
		||||
	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())
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 {
 | 
			
		||||
@@ -57,3 +52,71 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										245
									
								
								api/handler/dalle_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								api/handler/dalle_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,245 @@
 | 
			
		||||
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/dalle"
 | 
			
		||||
	"geekai/service/oss"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DallJobHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	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,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Image 创建一个绘画任务
 | 
			
		||||
func (h *DallJobHandler) Image(c *gin.Context) {
 | 
			
		||||
	var data types.DallTask
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chatModel model.ChatModel
 | 
			
		||||
	if res := h.DB.Where("id = ?", data.ModelId).First(&chatModel); res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "模型不存在")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查用户剩余算力
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if user.Power < chatModel.Power {
 | 
			
		||||
		resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	idValue, _ := c.Get(types.LoginUserID)
 | 
			
		||||
	userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
 | 
			
		||||
	task := types.DallTask{
 | 
			
		||||
		ClientId:         data.ClientId,
 | 
			
		||||
		UserId:           uint(userId),
 | 
			
		||||
		ModelId:          chatModel.Id,
 | 
			
		||||
		ModelName:        chatModel.Value,
 | 
			
		||||
		Prompt:           data.Prompt,
 | 
			
		||||
		Quality:          data.Quality,
 | 
			
		||||
		Size:             data.Size,
 | 
			
		||||
		Style:            data.Style,
 | 
			
		||||
		TranslateModelId: h.App.SysConfig.TranslateModelId,
 | 
			
		||||
		Power:            chatModel.Power,
 | 
			
		||||
	}
 | 
			
		||||
	job := model.DallJob{
 | 
			
		||||
		UserId:   uint(userId),
 | 
			
		||||
		Prompt:   data.Prompt,
 | 
			
		||||
		Power:    chatModel.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)
 | 
			
		||||
 | 
			
		||||
	// 扣减算力
 | 
			
		||||
	err = h.userService.DecreasePower(int(user.Id), chatModel.Power, model.PowerLog{
 | 
			
		||||
		Type:   types.PowerConsume,
 | 
			
		||||
		Model:  chatModel.Value,
 | 
			
		||||
		Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with decrease power: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ImgWall 照片墙
 | 
			
		||||
func (h *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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *DallJobHandler) GetModels(c *gin.Context) {
 | 
			
		||||
	var models []model.ChatModel
 | 
			
		||||
	err := h.DB.Where("type", "img").Where("enabled", true).Find(&models).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var modelVos []vo.ChatModel
 | 
			
		||||
	for _, v := range models {
 | 
			
		||||
		var modelVo vo.ChatModel
 | 
			
		||||
		err := utils.CopyObject(v, &modelVo)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		modelVo.Id = v.Id
 | 
			
		||||
		modelVos = append(modelVos, modelVo)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, modelVos)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										277
									
								
								api/handler/function_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								api/handler/function_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,277 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/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
 | 
			
		||||
	userService   *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFunctionHandler(
 | 
			
		||||
	server *core.AppServer,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	config *types.AppConfig,
 | 
			
		||||
	manager *oss.UploaderManager,
 | 
			
		||||
	dallService *dalle.Service,
 | 
			
		||||
	userService *service.UserService) *FunctionHandler {
 | 
			
		||||
	return &FunctionHandler{
 | 
			
		||||
		BaseHandler: BaseHandler{
 | 
			
		||||
			App: server,
 | 
			
		||||
			DB:  db,
 | 
			
		||||
		},
 | 
			
		||||
		config:        config.ApiConfig,
 | 
			
		||||
		uploadManager: manager,
 | 
			
		||||
		dallService:   dallService,
 | 
			
		||||
		userService:   userService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("%v", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("%v", r.Err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		resp.ERROR(c, res.Message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt))
 | 
			
		||||
	for _, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, v.Title)
 | 
			
		||||
	}
 | 
			
		||||
	builder = append(builder, res.Data.Title)
 | 
			
		||||
	resp.SUCCESS(c, strings.Join(builder, "\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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"])
 | 
			
		||||
	task := types.DallTask{
 | 
			
		||||
		UserId:           user.Id,
 | 
			
		||||
		Prompt:           prompt,
 | 
			
		||||
		ModelId:          0,
 | 
			
		||||
		ModelName:        "dall-e-3",
 | 
			
		||||
		TranslateModelId: h.App.SysConfig.TranslateModelId,
 | 
			
		||||
		N:                1,
 | 
			
		||||
		Quality:          "standard",
 | 
			
		||||
		Size:             "1024x1024",
 | 
			
		||||
		Style:            "vivid",
 | 
			
		||||
		Power:            h.App.SysConfig.DallPower,
 | 
			
		||||
	}
 | 
			
		||||
	job := model.DallJob{
 | 
			
		||||
		UserId:   user.Id,
 | 
			
		||||
		Prompt:   prompt,
 | 
			
		||||
		Power:    h.App.SysConfig.DallPower,
 | 
			
		||||
		TaskInfo: utils.JsonEncode(task),
 | 
			
		||||
	}
 | 
			
		||||
	err := h.DB.Create(&job).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	task.Id = job.Id
 | 
			
		||||
	content, err := h.dallService.Image(task, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "任务执行失败:"+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 扣减算力
 | 
			
		||||
	err = h.userService.DecreasePower(int(user.Id), job.Power, model.PowerLog{
 | 
			
		||||
		Type:   types.PowerConsume,
 | 
			
		||||
		Model:  task.ModelName,
 | 
			
		||||
		Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(job.Prompt, 10)),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "扣减算力失败:"+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List 获取所有的工具函数列表
 | 
			
		||||
func (h *FunctionHandler) List(c *gin.Context) {
 | 
			
		||||
	var items []model.Function
 | 
			
		||||
	err := h.DB.Where("enabled", true).Find(&items).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tools := make([]vo.Function, 0)
 | 
			
		||||
	for _, v := range items {
 | 
			
		||||
		var f vo.Function
 | 
			
		||||
		err = utils.CopyObject(v, &f)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		f.Action = ""
 | 
			
		||||
		f.Token = ""
 | 
			
		||||
		tools = append(tools, f)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, tools)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,239 +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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/function"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"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"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TaskStatus string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	Start    = TaskStatus("Started")
 | 
			
		||||
	Running  = TaskStatus("Running")
 | 
			
		||||
	Stopped  = TaskStatus("Stopped")
 | 
			
		||||
	Finished = TaskStatus("Finished")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Image struct {
 | 
			
		||||
	URL      string `json:"url"`
 | 
			
		||||
	ProxyURL string `json:"proxy_url"`
 | 
			
		||||
	Filename string `json:"filename"`
 | 
			
		||||
	Width    int    `json:"width"`
 | 
			
		||||
	Height   int    `json:"height"`
 | 
			
		||||
	Size     int    `json:"size"`
 | 
			
		||||
	Hash     string `json:"hash"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MidJourneyHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	leveldb         *store.LevelDB
 | 
			
		||||
	db              *gorm.DB
 | 
			
		||||
	mjFunc          function.FuncMidJourney
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
	mjService   *mj.Service
 | 
			
		||||
	snowflake   *service.Snowflake
 | 
			
		||||
	uploader    *oss.UploaderManager
 | 
			
		||||
	userService *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMidJourneyHandler(
 | 
			
		||||
	app *core.AppServer,
 | 
			
		||||
	leveldb *store.LevelDB,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	manager *oss.UploaderManager,
 | 
			
		||||
	functions map[string]function.Function) *MidJourneyHandler {
 | 
			
		||||
	h := MidJourneyHandler{
 | 
			
		||||
		leveldb:         leveldb,
 | 
			
		||||
		db:              db,
 | 
			
		||||
		uploaderManager: manager,
 | 
			
		||||
		mjFunc:          functions[types.FuncMidJourney].(function.FuncMidJourney)}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
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) Notify(c *gin.Context) {
 | 
			
		||||
	token := c.GetHeader("Authorization")
 | 
			
		||||
	if token != h.App.Config.ExtConfig.Token {
 | 
			
		||||
func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var data struct {
 | 
			
		||||
		MessageId   string     `json:"message_id"`
 | 
			
		||||
		ReferenceId string     `json:"reference_id"`
 | 
			
		||||
		Image       Image      `json:"image"`
 | 
			
		||||
		Content     string     `json:"content"`
 | 
			
		||||
		Prompt      string     `json:"prompt"`
 | 
			
		||||
		Status      TaskStatus `json:"status"`
 | 
			
		||||
		Key         string     `json:"key"`
 | 
			
		||||
	if user.Power < h.App.SysConfig.MjPower {
 | 
			
		||||
		resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Debugf("收到 MidJourney 回调请求:%+v", data)
 | 
			
		||||
 | 
			
		||||
	// the job is saved
 | 
			
		||||
	var job model.MidJourneyJob
 | 
			
		||||
	res := h.db.Where("message_id = ?", data.MessageId).First(&job)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	if !h.preCheck(c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data.Key = utils.Sha256(data.Prompt)
 | 
			
		||||
	//logger.Info(data.Prompt, ",", key)
 | 
			
		||||
	if data.Status == Finished {
 | 
			
		||||
		var task types.MjTask
 | 
			
		||||
		err := h.leveldb.Get(types.TaskStorePrefix+data.Key, &task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("error with get MidJourney task: ", err)
 | 
			
		||||
			resp.SUCCESS(c)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// download image
 | 
			
		||||
		imgURL, err := h.uploaderManager.GetActiveService().PutImg(data.Image.URL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("error with download image: ", err)
 | 
			
		||||
			resp.SUCCESS(c)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Image.URL = imgURL
 | 
			
		||||
		message := model.HistoryMessage{
 | 
			
		||||
			UserId:     task.UserId,
 | 
			
		||||
			ChatId:     task.ChatId,
 | 
			
		||||
			RoleId:     task.RoleId,
 | 
			
		||||
			Type:       types.MjMsg,
 | 
			
		||||
			Icon:       task.Icon,
 | 
			
		||||
			Content:    utils.JsonEncode(data),
 | 
			
		||||
			Tokens:     0,
 | 
			
		||||
			UseContext: false,
 | 
			
		||||
		}
 | 
			
		||||
		res := h.db.Create(&message)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			logger.Error("error with save chat history message: ", res.Error)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// save the job
 | 
			
		||||
		job.UserId = task.UserId
 | 
			
		||||
		job.ChatId = task.ChatId
 | 
			
		||||
		job.MessageId = data.MessageId
 | 
			
		||||
		job.ReferenceId = data.ReferenceId
 | 
			
		||||
		job.Content = data.Content
 | 
			
		||||
		job.Prompt = data.Prompt
 | 
			
		||||
		job.Image = utils.JsonEncode(data.Image)
 | 
			
		||||
		job.Hash = data.Image.Hash
 | 
			
		||||
		job.CreatedAt = time.Now()
 | 
			
		||||
		res = h.db.Create(&job)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			logger.Error("error with save MidJourney Job: ", res.Error)
 | 
			
		||||
	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"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 推送消息到客户端
 | 
			
		||||
	wsClient := h.App.MjTaskClients.Get(data.Key)
 | 
			
		||||
	if wsClient == nil { // 客户端断线,则丢弃
 | 
			
		||||
		logger.Errorf("Client is offline: %+v", data)
 | 
			
		||||
		resp.SUCCESS(c, "Client is offline")
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if data.Status == Finished {
 | 
			
		||||
		utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
 | 
			
		||||
		utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
 | 
			
		||||
		// delete client
 | 
			
		||||
		h.App.MjTaskClients.Delete(data.Key)
 | 
			
		||||
	} else {
 | 
			
		||||
		// 使用代理临时转发图片
 | 
			
		||||
		if data.Image.URL != "" {
 | 
			
		||||
			image, err := utils.DownloadImage(data.Image.URL, h.App.Config.ProxyURL)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
 | 
			
		||||
	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, "SUCCESS")
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type reqVo struct {
 | 
			
		||||
	Index       int32  `json:"index"`
 | 
			
		||||
	Index       int    `json:"index"`
 | 
			
		||||
	ClientId    string `json:"client_id"`
 | 
			
		||||
	ChannelId   string `json:"channel_id"`
 | 
			
		||||
	MessageId   string `json:"message_id"`
 | 
			
		||||
	MessageHash string `json:"message_hash"`
 | 
			
		||||
	SessionId   string `json:"session_id"`
 | 
			
		||||
	Key         string `json:"key"`
 | 
			
		||||
	Prompt      string `json:"prompt"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Upscale send upscale command to MidJourney Bot
 | 
			
		||||
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
 | 
			
		||||
	var data reqVo
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil ||
 | 
			
		||||
		data.SessionId == "" ||
 | 
			
		||||
		data.Key == "" {
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	wsClient := h.App.ChatClients.Get(data.SessionId)
 | 
			
		||||
	if wsClient == nil {
 | 
			
		||||
		resp.ERROR(c, "No Websocket client online")
 | 
			
		||||
 | 
			
		||||
	if !h.preCheck(c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := h.mjFunc.Upscale(function.MjUpscaleReq{
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content := fmt.Sprintf("**%s** 已推送 Upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
 | 
			
		||||
	utils.ReplyMessage(wsClient, content)
 | 
			
		||||
	if h.App.MjTaskClients.Get(data.Key) == nil {
 | 
			
		||||
		h.App.MjTaskClients.Put(data.Key, wsClient)
 | 
			
		||||
	}
 | 
			
		||||
	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 ||
 | 
			
		||||
		data.SessionId == "" ||
 | 
			
		||||
		data.Key == "" {
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	wsClient := h.App.ChatClients.Get(data.SessionId)
 | 
			
		||||
	if wsClient == nil {
 | 
			
		||||
		resp.ERROR(c, "No Websocket client online")
 | 
			
		||||
 | 
			
		||||
	if !h.preCheck(c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := h.mjFunc.Variation(function.MjVariationReq{
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
	content := fmt.Sprintf("**%s** 已推送 Variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
 | 
			
		||||
	utils.ReplyMessage(wsClient, content)
 | 
			
		||||
	if h.App.MjTaskClients.Get(data.Key) == nil {
 | 
			
		||||
		h.App.MjTaskClients.Put(data.Key, wsClient)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								api/handler/power_log_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								api/handler/power_log_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PowerLogHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
 | 
			
		||||
	return &PowerLogHandler{BaseHandler: BaseHandler{App: app, DB: db}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *PowerLogHandler) List(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Model    string   `json:"model"`
 | 
			
		||||
		Date     []string `json:"date"`
 | 
			
		||||
		Page     int      `json:"page"`
 | 
			
		||||
		PageSize int      `json:"page_size"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	session = session.Where("user_id", userId)
 | 
			
		||||
	if data.Model != "" {
 | 
			
		||||
		session = session.Where("model", data.Model)
 | 
			
		||||
	}
 | 
			
		||||
	if len(data.Date) == 2 {
 | 
			
		||||
		start := data.Date[0] + " 00:00:00"
 | 
			
		||||
		end := data.Date[1] + " 00:00:00"
 | 
			
		||||
		session = session.Where("created_at >= ? AND created_at <= ?", start, end)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var total int64
 | 
			
		||||
	session.Model(&model.PowerLog{}).Count(&total)
 | 
			
		||||
	var items []model.PowerLog
 | 
			
		||||
	var list = make([]vo.PowerLog, 0)
 | 
			
		||||
	offset := (data.Page - 1) * data.PageSize
 | 
			
		||||
	res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var log vo.PowerLog
 | 
			
		||||
			err := utils.CopyObject(item, &log)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			log.Id = item.Id
 | 
			
		||||
			log.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
			log.TypeStr = item.Type.String()
 | 
			
		||||
			list = append(list, log)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								api/handler/product_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								api/handler/product_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ProductHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
 | 
			
		||||
	return &ProductHandler{BaseHandler: BaseHandler{App: app, DB: db}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List 模型列表
 | 
			
		||||
func (h *ProductHandler) List(c *gin.Context) {
 | 
			
		||||
	var items []model.Product
 | 
			
		||||
	var list = make([]vo.Product, 0)
 | 
			
		||||
	res := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var product vo.Product
 | 
			
		||||
			err := utils.CopyObject(item, &product)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				product.Id = item.Id
 | 
			
		||||
				product.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
				product.UpdatedAt = item.UpdatedAt.Unix()
 | 
			
		||||
				list = append(list, product)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, list)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										155
									
								
								api/handler/prompt_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								api/handler/prompt_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
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"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 提示词生成 handler
 | 
			
		||||
// 使用 AI 生成绘画指令,歌词,视频生成指令等
 | 
			
		||||
 | 
			
		||||
type PromptHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	userService *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPromptHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *PromptHandler {
 | 
			
		||||
	return &PromptHandler{
 | 
			
		||||
		BaseHandler: BaseHandler{
 | 
			
		||||
			App: app,
 | 
			
		||||
			DB:  db,
 | 
			
		||||
		},
 | 
			
		||||
		userService: userService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lyric 生成歌词
 | 
			
		||||
func (h *PromptHandler) Lyric(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Prompt string `json:"prompt"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.App.SysConfig.PromptPower > 0 {
 | 
			
		||||
		userId := h.GetLoginUserId(c)
 | 
			
		||||
		h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
 | 
			
		||||
			Type:   types.PowerConsume,
 | 
			
		||||
			Model:  h.getPromptModel(),
 | 
			
		||||
			Remark: "生成歌词",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Image 生成 AI 绘画提示词
 | 
			
		||||
func (h *PromptHandler) Image(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Prompt string `json:"prompt"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if h.App.SysConfig.PromptPower > 0 {
 | 
			
		||||
		userId := h.GetLoginUserId(c)
 | 
			
		||||
		h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
 | 
			
		||||
			Type:   types.PowerConsume,
 | 
			
		||||
			Model:  h.getPromptModel(),
 | 
			
		||||
			Remark: "生成绘画提示词",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, strings.Trim(content, `"`))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Video 生成视频提示词
 | 
			
		||||
func (h *PromptHandler) Video(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Prompt string `json:"prompt"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.App.SysConfig.PromptPower > 0 {
 | 
			
		||||
		userId := h.GetLoginUserId(c)
 | 
			
		||||
		h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
 | 
			
		||||
			Type:   types.PowerConsume,
 | 
			
		||||
			Model:  h.getPromptModel(),
 | 
			
		||||
			Remark: "生成视频脚本",
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, strings.Trim(content, `"`))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MetaPrompt 生成元提示词
 | 
			
		||||
func (h *PromptHandler) MetaPrompt(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Prompt string `json:"prompt"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	messages := make([]interface{}, 0)
 | 
			
		||||
	messages = append(messages, types.Message{
 | 
			
		||||
		Role:    "system",
 | 
			
		||||
		Content: service.MetaPromptTemplate,
 | 
			
		||||
	})
 | 
			
		||||
	messages = append(messages, types.Message{
 | 
			
		||||
		Role:    "user",
 | 
			
		||||
		Content: "Task, Goal, or the Role to actor is:\n" + data.Prompt,
 | 
			
		||||
	})
 | 
			
		||||
	content, err := utils.SendOpenAIMessage(h.DB, messages, 0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, strings.Trim(content, `"`))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *PromptHandler) getPromptModel() string {
 | 
			
		||||
	if h.App.SysConfig.TranslateModelId > 0 {
 | 
			
		||||
		var chatModel model.ChatModel
 | 
			
		||||
		h.DB.Where("id", h.App.SysConfig.TranslateModelId).First(&chatModel)
 | 
			
		||||
		return chatModel.Value
 | 
			
		||||
	}
 | 
			
		||||
	return "gpt-4o"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										221
									
								
								api/handler/realtime_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								api/handler/realtime_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,221 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
// OpenAI Realtime API Relay Server
 | 
			
		||||
 | 
			
		||||
type RealtimeHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	userService *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRealtimeHandler(server *core.AppServer, db *gorm.DB, userService *service.UserService) *RealtimeHandler {
 | 
			
		||||
	return &RealtimeHandler{BaseHandler: BaseHandler{App: server, DB: db}, userService: userService}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *RealtimeHandler) Connection(c *gin.Context) {
 | 
			
		||||
	// 获取客户端请求中指定的子协议
 | 
			
		||||
	clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
 | 
			
		||||
	md := c.Query("model")
 | 
			
		||||
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	var user model.User
 | 
			
		||||
	if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 将 HTTP 协议升级为 Websocket 协议
 | 
			
		||||
	subProtocols := strings.Split(clientProtocols, ",")
 | 
			
		||||
	ws, err := (&websocket.Upgrader{
 | 
			
		||||
		CheckOrigin:  func(r *http.Request) bool { return true },
 | 
			
		||||
		Subprotocols: subProtocols,
 | 
			
		||||
	}).Upgrade(c.Writer, c.Request, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer ws.Close()
 | 
			
		||||
 | 
			
		||||
	// 目前只针对 VIP 用户可以访问
 | 
			
		||||
	if !user.Vip {
 | 
			
		||||
		sendError(ws, "当前功能只针对 VIP 用户开放")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var apiKey model.ApiKey
 | 
			
		||||
	h.DB.Where("type", "realtime").Where("enabled", true).Order("last_used_at ASC").First(&apiKey)
 | 
			
		||||
	if apiKey.Id == 0 {
 | 
			
		||||
		sendError(ws, "管理员未配置 Realtime API KEY")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/v1/realtime?model=%s", apiKey.ApiURL, md)
 | 
			
		||||
	// 连接到真实的后端服务器,传入相同的子协议
 | 
			
		||||
	headers := http.Header{}
 | 
			
		||||
	// 修正子协议内容
 | 
			
		||||
	subProtocols[1] = "openai-insecure-api-key." + apiKey.Value
 | 
			
		||||
	if clientProtocols != "" {
 | 
			
		||||
		headers.Set("Sec-WebSocket-Protocol", strings.Join(subProtocols, ","))
 | 
			
		||||
	}
 | 
			
		||||
	backendConn, _, err := websocket.DefaultDialer.Dial(apiURL, headers)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		sendError(ws, "桥接后端 API 失败:"+err.Error())
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer backendConn.Close()
 | 
			
		||||
 | 
			
		||||
	// 确保协议一致性,如果失败返回
 | 
			
		||||
	if ws.Subprotocol() != backendConn.Subprotocol() {
 | 
			
		||||
		sendError(ws, "Websocket 子协议不匹配")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新API KEY 最后使用时间
 | 
			
		||||
	h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
 | 
			
		||||
	// 开始双向转发
 | 
			
		||||
	errorChan := make(chan error, 2)
 | 
			
		||||
	go relay(ws, backendConn, errorChan)
 | 
			
		||||
	go relay(backendConn, ws, errorChan)
 | 
			
		||||
 | 
			
		||||
	// 等待其中一个连接关闭
 | 
			
		||||
	err = <-errorChan
 | 
			
		||||
	logger.Infof("Relay ended: %v", err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func relay(src, dst *websocket.Conn, errorChan chan error) {
 | 
			
		||||
	for {
 | 
			
		||||
		messageType, message, err := src.ReadMessage()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errorChan <- err
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		err = dst.WriteMessage(messageType, message)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errorChan <- err
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sendError(ws *websocket.Conn, message string) {
 | 
			
		||||
	err := ws.WriteJSON(map[string]string{"event_id": "event_01", "type": "error", "error": message})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OpenAI 实时语音对话,一次性对话
 | 
			
		||||
func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
 | 
			
		||||
	var apiKey model.ApiKey
 | 
			
		||||
	err := h.DB.Session(&gorm.Session{}).Where("type", "realtime").Where("enabled", true).First(&apiKey).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("error with fetch OpenAI API KEY:%v", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查用户是否还有算力
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	var user model.User
 | 
			
		||||
	if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("error with fetch user:%v", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.Power < h.App.SysConfig.AdvanceVoicePower {
 | 
			
		||||
		resp.ERROR(c, "当前用户算力不足,无法使用该功能")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var response utils.OpenAIResponse
 | 
			
		||||
	client := req.C()
 | 
			
		||||
	if len(apiKey.ProxyURL) > 5 {
 | 
			
		||||
		client.SetProxyURL(apiKey.ApiURL)
 | 
			
		||||
	}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
 | 
			
		||||
	logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, "advanced-voice")
 | 
			
		||||
	r, err := client.R().SetHeader("Body-Type", "application/json").
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+apiKey.Value).
 | 
			
		||||
		SetBody(types.ApiRequest{
 | 
			
		||||
			Model:       "advanced-voice",
 | 
			
		||||
			Temperature: 0.9,
 | 
			
		||||
			MaxTokens:   1024,
 | 
			
		||||
			Stream:      false,
 | 
			
		||||
			Messages: []interface{}{types.Message{
 | 
			
		||||
				Role:    "user",
 | 
			
		||||
				Content: "实时语音通话",
 | 
			
		||||
			}},
 | 
			
		||||
		}).Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("请求 OpenAI API失败:%v", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("请求 OpenAI API失败:%v", r.Status))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, _ := io.ReadAll(r.Body)
 | 
			
		||||
	err = json.Unmarshal(body, &response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("解析API数据失败:%v, %s", err, string(body)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新 API KEY 的最后使用时间
 | 
			
		||||
	h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
 | 
			
		||||
	// 扣减算力
 | 
			
		||||
	err = h.userService.DecreasePower(int(userId), h.App.SysConfig.AdvanceVoicePower, model.PowerLog{
 | 
			
		||||
		Type:   types.PowerConsume,
 | 
			
		||||
		Model:  "advanced-voice",
 | 
			
		||||
		Remark: "实时语音通话",
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Infof("Response: %v", response.Choices[0].Message.Content)
 | 
			
		||||
 | 
			
		||||
	// 提取链接
 | 
			
		||||
	re := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
 | 
			
		||||
	links := re.FindAllStringSubmatch(response.Choices[0].Message.Content, -1)
 | 
			
		||||
	var url = ""
 | 
			
		||||
	if len(links) > 0 {
 | 
			
		||||
		url = links[0][2]
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, url)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										88
									
								
								api/handler/redeem_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								api/handler/redeem_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
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/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RedeemHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	lock        sync.Mutex
 | 
			
		||||
	userService *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRedeemHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *RedeemHandler {
 | 
			
		||||
	return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}, userService: userService}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *RedeemHandler) Verify(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Code string `json:"code"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
 | 
			
		||||
	h.lock.Lock()
 | 
			
		||||
	defer h.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	var item model.Redeem
 | 
			
		||||
	res := h.DB.Where("code", data.Code).First(&item)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "无效的兑换码!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !item.Enabled {
 | 
			
		||||
		resp.ERROR(c, "当前兑换码已被禁用!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if item.RedeemedAt > 0 {
 | 
			
		||||
		resp.ERROR(c, "当前兑换码已使用,请勿重复使用!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tx := h.DB.Begin()
 | 
			
		||||
	err := h.userService.IncreasePower(int(userId), item.Power, model.PowerLog{
 | 
			
		||||
		Type:   types.PowerRedeem,
 | 
			
		||||
		Model:  "兑换码",
 | 
			
		||||
		Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新核销状态
 | 
			
		||||
	item.RedeemedAt = time.Now().Unix()
 | 
			
		||||
	item.UserId = userId
 | 
			
		||||
	err = tx.Updates(&item).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tx.Commit()
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,117 +0,0 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RewardHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
 | 
			
		||||
	h := RewardHandler{db: db}
 | 
			
		||||
	h.App = server
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *RewardHandler) Notify(c *gin.Context) {
 | 
			
		||||
	token := c.GetHeader("Authorization")
 | 
			
		||||
	if token != h.App.Config.ExtConfig.Token {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var data struct {
 | 
			
		||||
		TransId string  `json:"trans_id"` // 微信转账交易 ID
 | 
			
		||||
		Amount  float64 `json:"amount"`   // 微信转账交易金额
 | 
			
		||||
		Remark  string  `json:"remark"`   // 转账备注
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if data.Amount <= 0 {
 | 
			
		||||
		resp.ERROR(c, "Amount should not be 0")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Infof("收到众筹收款信息: %+v", data)
 | 
			
		||||
	var item model.Reward
 | 
			
		||||
	res := h.db.Where("tx_id = ?", data.TransId).First(&item)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.ERROR(c, "当前交易 ID 己经存在!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res = h.db.Create(&model.Reward{
 | 
			
		||||
		TxId:   data.TransId,
 | 
			
		||||
		Amount: data.Amount,
 | 
			
		||||
		Remark: data.Remark,
 | 
			
		||||
		Status: false,
 | 
			
		||||
	})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		logger.Errorf("交易保存失败: %v", res.Error)
 | 
			
		||||
		resp.ERROR(c, "交易保存失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Verify 打赏码核销
 | 
			
		||||
func (h *RewardHandler) Verify(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		TxId string `json:"tx_id"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var item model.Reward
 | 
			
		||||
	res := h.db.Where("tx_id = ?", data.TxId).First(&item)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "无效的众筹交易流水号!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if item.Status {
 | 
			
		||||
		resp.ERROR(c, "当前众筹交易流水号已经被核销,请不要重复核销!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.HACKER(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tx := h.db.Begin()
 | 
			
		||||
	calls := (item.Amount + 0.1) * 10
 | 
			
		||||
	res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls))
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新核销状态
 | 
			
		||||
	item.Status = true
 | 
			
		||||
	item.UserId = user.Id
 | 
			
		||||
	res = h.db.Updates(&item)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tx.Commit()
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										283
									
								
								api/handler/sd_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								api/handler/sd_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,283 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/oss"
 | 
			
		||||
	"geekai/service/sd"
 | 
			
		||||
	"geekai/store"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type SdJobHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	redis       *redis.Client
 | 
			
		||||
	sdService   *sd.Service
 | 
			
		||||
	uploader    *oss.UploaderManager
 | 
			
		||||
	snowflake   *service.Snowflake
 | 
			
		||||
	leveldb     *store.LevelDB
 | 
			
		||||
	userService *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSdJobHandler(app *core.AppServer,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	service *sd.Service,
 | 
			
		||||
	manager *oss.UploaderManager,
 | 
			
		||||
	snowflake *service.Snowflake,
 | 
			
		||||
	userService *service.UserService,
 | 
			
		||||
	levelDB *store.LevelDB) *SdJobHandler {
 | 
			
		||||
	return &SdJobHandler{
 | 
			
		||||
		sdService:   service,
 | 
			
		||||
		uploader:    manager,
 | 
			
		||||
		snowflake:   snowflake,
 | 
			
		||||
		leveldb:     levelDB,
 | 
			
		||||
		userService: userService,
 | 
			
		||||
		BaseHandler: BaseHandler{
 | 
			
		||||
			App: app,
 | 
			
		||||
			DB:  db,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *SdJobHandler) preCheck(c *gin.Context) bool {
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.Power < h.App.SysConfig.SdPower {
 | 
			
		||||
		resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Image 创建一个绘画任务
 | 
			
		||||
func (h *SdJobHandler) Image(c *gin.Context) {
 | 
			
		||||
	if !h.preCheck(c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var data types.SdTaskParams
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if data.Width <= 0 {
 | 
			
		||||
		data.Width = 512
 | 
			
		||||
	}
 | 
			
		||||
	if data.Height <= 0 {
 | 
			
		||||
		data.Height = 512
 | 
			
		||||
	}
 | 
			
		||||
	if data.CfgScale <= 0 {
 | 
			
		||||
		data.CfgScale = 7
 | 
			
		||||
	}
 | 
			
		||||
	if data.Seed == 0 {
 | 
			
		||||
		data.Seed = -1
 | 
			
		||||
	}
 | 
			
		||||
	if data.Steps <= 0 {
 | 
			
		||||
		data.Steps = 20
 | 
			
		||||
	}
 | 
			
		||||
	if data.Sampler == "" {
 | 
			
		||||
		data.Sampler = "Euler a"
 | 
			
		||||
	}
 | 
			
		||||
	idValue, _ := c.Get(types.LoginUserID)
 | 
			
		||||
	userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
 | 
			
		||||
	taskId, err := h.snowflake.Next(true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with generate task id: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	task := types.SdTask{
 | 
			
		||||
		ClientId: data.ClientId,
 | 
			
		||||
		Type:     types.TaskImage,
 | 
			
		||||
		Params: types.SdTaskParams{
 | 
			
		||||
			TaskId:       taskId,
 | 
			
		||||
			Prompt:       data.Prompt,
 | 
			
		||||
			NegPrompt:    data.NegPrompt,
 | 
			
		||||
			Steps:        data.Steps,
 | 
			
		||||
			Sampler:      data.Sampler,
 | 
			
		||||
			FaceFix:      data.FaceFix,
 | 
			
		||||
			CfgScale:     data.CfgScale,
 | 
			
		||||
			Seed:         data.Seed,
 | 
			
		||||
			Height:       data.Height,
 | 
			
		||||
			Width:        data.Width,
 | 
			
		||||
			HdFix:        data.HdFix,
 | 
			
		||||
			HdRedrawRate: data.HdRedrawRate,
 | 
			
		||||
			HdScale:      data.HdScale,
 | 
			
		||||
			HdScaleAlg:   data.HdScaleAlg,
 | 
			
		||||
			HdSteps:      data.HdSteps,
 | 
			
		||||
		},
 | 
			
		||||
		UserId:           userId,
 | 
			
		||||
		TranslateModelId: h.App.SysConfig.TranslateModelId,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	job := model.SdJob{
 | 
			
		||||
		UserId:    userId,
 | 
			
		||||
		Type:      types.TaskImage.String(),
 | 
			
		||||
		TaskId:    taskId,
 | 
			
		||||
		Params:    utils.JsonEncode(task.Params),
 | 
			
		||||
		TaskInfo:  utils.JsonEncode(task),
 | 
			
		||||
		Prompt:    data.Prompt,
 | 
			
		||||
		Progress:  0,
 | 
			
		||||
		Power:     h.App.SysConfig.SdPower,
 | 
			
		||||
		CreatedAt: time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
	res := h.DB.Create(&job)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "error with save job: "+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	task.Id = int(job.Id)
 | 
			
		||||
	h.sdService.PushTask(task)
 | 
			
		||||
 | 
			
		||||
	// update user's power
 | 
			
		||||
	err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
 | 
			
		||||
		Type:   types.PowerConsume,
 | 
			
		||||
		Model:  "stable-diffusion",
 | 
			
		||||
		Remark: fmt.Sprintf("绘图操作,任务ID:%s", job.TaskId),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ImgWall 照片墙
 | 
			
		||||
func (h *SdJobHandler) ImgWall(c *gin.Context) {
 | 
			
		||||
	page := h.GetInt(c, "page", 0)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 0)
 | 
			
		||||
	err, jobs := h.getData(true, 0, page, pageSize, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, jobs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JobList 获取 SD 任务列表
 | 
			
		||||
func (h *SdJobHandler) JobList(c *gin.Context) {
 | 
			
		||||
	finish := h.GetBool(c, "finish")
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	page := h.GetInt(c, "page", 0)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 0)
 | 
			
		||||
	publish := h.GetBool(c, "publish")
 | 
			
		||||
 | 
			
		||||
	err, jobs := h.getData(finish, userId, page, pageSize, publish)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, jobs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JobList 获取 MJ 任务列表
 | 
			
		||||
func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
 | 
			
		||||
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	if finish {
 | 
			
		||||
		session = session.Where("progress >= ?", 100).Order("id DESC")
 | 
			
		||||
	} else {
 | 
			
		||||
		session = session.Where("progress < ?", 100).Order("id ASC")
 | 
			
		||||
	}
 | 
			
		||||
	if userId > 0 {
 | 
			
		||||
		session = session.Where("user_id = ?", userId)
 | 
			
		||||
	}
 | 
			
		||||
	if publish {
 | 
			
		||||
		session = session.Where("publish", publish)
 | 
			
		||||
	}
 | 
			
		||||
	if page > 0 && pageSize > 0 {
 | 
			
		||||
		offset := (page - 1) * pageSize
 | 
			
		||||
		session = session.Offset(offset).Limit(pageSize)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 统计总数
 | 
			
		||||
	var total int64
 | 
			
		||||
	session.Model(&model.SdJob{}).Count(&total)
 | 
			
		||||
 | 
			
		||||
	var items []model.SdJob
 | 
			
		||||
	res := session.Find(&items)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return res.Error, vo.Page{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var jobs = make([]vo.SdJob, 0)
 | 
			
		||||
	for _, item := range items {
 | 
			
		||||
		var job vo.SdJob
 | 
			
		||||
		err := utils.CopyObject(item, &job)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		jobs = append(jobs, job)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, vo.NewPage(total, page, pageSize, jobs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove remove task image
 | 
			
		||||
func (h *SdJobHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	var job model.SdJob
 | 
			
		||||
	if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "记录不存在")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 删除任务
 | 
			
		||||
	err := h.DB.Delete(&job).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// remove image
 | 
			
		||||
	err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("remove image failed: ", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Publish 发布/取消发布图片到画廊显示
 | 
			
		||||
func (h *SdJobHandler) Publish(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
 | 
			
		||||
 | 
			
		||||
	err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: int(userId)}).UpdateColumn("publish", action).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,70 +1,119 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/sms"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const CodeStorePrefix = "/verify/codes/"
 | 
			
		||||
 | 
			
		||||
type SmsHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	leveldb *store.LevelDB
 | 
			
		||||
	sms     *service.AliYunSmsService
 | 
			
		||||
	redis   *redis.Client
 | 
			
		||||
	sms     *sms.ServiceManager
 | 
			
		||||
	smtp    *service.SmtpService
 | 
			
		||||
	captcha *service.CaptchaService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
 | 
			
		||||
	handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
func NewSmsHandler(
 | 
			
		||||
	app *core.AppServer,
 | 
			
		||||
	client *redis.Client,
 | 
			
		||||
	sms *sms.ServiceManager,
 | 
			
		||||
	smtp *service.SmtpService,
 | 
			
		||||
	captcha *service.CaptchaService) *SmsHandler {
 | 
			
		||||
	return &SmsHandler{
 | 
			
		||||
		redis:       client,
 | 
			
		||||
		sms:         sms,
 | 
			
		||||
		captcha:     captcha,
 | 
			
		||||
		smtp:        smtp,
 | 
			
		||||
		BaseHandler: BaseHandler{App: app}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendCode 发送验证码短信
 | 
			
		||||
// SendCode 发送验证码
 | 
			
		||||
func (h *SmsHandler) SendCode(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Key    string `json:"key"`
 | 
			
		||||
		Dots   string `json:"dots"`
 | 
			
		||||
		Receiver string `json:"receiver"` // 接收者
 | 
			
		||||
		Key      string `json:"key"`
 | 
			
		||||
		Dots     string `json:"dots,omitempty"`
 | 
			
		||||
		X        int    `json:"x,omitempty"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !h.captcha.Check(data) {
 | 
			
		||||
		resp.ERROR(c, "验证码错误,请先完人机验证")
 | 
			
		||||
		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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	code := utils.RandomNumber(6)
 | 
			
		||||
	err := h.sms.SendVerifyCode(data.Mobile, code)
 | 
			
		||||
	var err error
 | 
			
		||||
	if strings.Contains(data.Receiver, "@") { // email
 | 
			
		||||
		if !utils.Contains(h.App.SysConfig.RegisterWays, "email") {
 | 
			
		||||
			resp.ERROR(c, "系统已禁用邮箱注册!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// 检查邮箱后缀是否在白名单
 | 
			
		||||
		if len(h.App.SysConfig.EmailWhiteList) > 0 {
 | 
			
		||||
			inWhiteList := false
 | 
			
		||||
			for _, suffix := range h.App.SysConfig.EmailWhiteList {
 | 
			
		||||
				if strings.HasSuffix(data.Receiver, suffix) {
 | 
			
		||||
					inWhiteList = true
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if !inWhiteList {
 | 
			
		||||
				resp.ERROR(c, "邮箱后缀不在白名单中")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		err = h.smtp.SendVerifyCode(data.Receiver, code)
 | 
			
		||||
	} else {
 | 
			
		||||
		if !utils.Contains(h.App.SysConfig.RegisterWays, "mobile") {
 | 
			
		||||
			resp.ERROR(c, "系统已禁用手机号注册!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		err = h.sms.GetService().SendVerifyCode(data.Receiver, code)
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 存储验证码,等待后面注册验证
 | 
			
		||||
	err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code)
 | 
			
		||||
	_, err = h.redis.Set(c, CodeStorePrefix+data.Receiver, code, 0).Result()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "验证码保存失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type statusVo struct {
 | 
			
		||||
	EnabledMsgService bool `json:"enabled_msg_service"`
 | 
			
		||||
	EnabledRegister   bool `json:"enabled_register"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Status check if the message service is enabled
 | 
			
		||||
func (h *SmsHandler) Status(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsgService, EnabledRegister: h.App.SysConfig.EnabledRegister})
 | 
			
		||||
	if h.App.Debug {
 | 
			
		||||
		resp.SUCCESS(c, code)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										325
									
								
								api/handler/suno_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								api/handler/suno_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,325 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/oss"
 | 
			
		||||
	"geekai/service/suno"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type SunoHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	sunoService *suno.Service
 | 
			
		||||
	uploader    *oss.UploaderManager
 | 
			
		||||
	userService *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService) *SunoHandler {
 | 
			
		||||
	return &SunoHandler{
 | 
			
		||||
		BaseHandler: BaseHandler{
 | 
			
		||||
			App: app,
 | 
			
		||||
			DB:  db,
 | 
			
		||||
		},
 | 
			
		||||
		sunoService: service,
 | 
			
		||||
		uploader:    uploader,
 | 
			
		||||
		userService: userService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *SunoHandler) Create(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	var data struct {
 | 
			
		||||
		ClientId     string `json:"client_id"`
 | 
			
		||||
		Prompt       string `json:"prompt"`
 | 
			
		||||
		Instrumental bool   `json:"instrumental"`
 | 
			
		||||
		Lyrics       string `json:"lyrics"`
 | 
			
		||||
		Model        string `json:"model"`
 | 
			
		||||
		Tags         string `json:"tags"`
 | 
			
		||||
		Title        string `json:"title"`
 | 
			
		||||
		Type         int    `json:"type"`
 | 
			
		||||
		RefTaskId    string `json:"ref_task_id"`         // 续写的任务id
 | 
			
		||||
		ExtendSecs   int    `json:"extend_secs"`         // 续写秒数
 | 
			
		||||
		RefSongId    string `json:"ref_song_id"`         // 续写的歌曲id
 | 
			
		||||
		SongId       string `json:"song_id,omitempty"`   // 要拼接的歌曲id
 | 
			
		||||
		AudioURL     string `json:"audio_url,omitempty"` // 上传自己创作的歌曲
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.Power < h.App.SysConfig.SunoPower {
 | 
			
		||||
		resp.ERROR(c, "您的算力不足,请充值后再试!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 歌曲拼接
 | 
			
		||||
	if data.SongId != "" && data.Type == 3 {
 | 
			
		||||
		var song model.SunoJob
 | 
			
		||||
		if err := h.DB.Where("song_id = ?", data.SongId).First(&song).Error; err == nil {
 | 
			
		||||
			data.Instrumental = song.Instrumental
 | 
			
		||||
			data.Model = song.ModelName
 | 
			
		||||
			data.Tags = song.Tags
 | 
			
		||||
		}
 | 
			
		||||
		// 拼接歌词
 | 
			
		||||
		var refSong model.SunoJob
 | 
			
		||||
		if err := h.DB.Where("song_id = ?", data.RefSongId).First(&refSong).Error; err == nil {
 | 
			
		||||
			data.Prompt = fmt.Sprintf("%s\n%s", song.Prompt, refSong.Prompt)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	task := types.SunoTask{
 | 
			
		||||
		ClientId:     data.ClientId,
 | 
			
		||||
		UserId:       int(h.GetLoginUserId(c)),
 | 
			
		||||
		Type:         data.Type,
 | 
			
		||||
		Title:        data.Title,
 | 
			
		||||
		RefTaskId:    data.RefTaskId,
 | 
			
		||||
		RefSongId:    data.RefSongId,
 | 
			
		||||
		ExtendSecs:   data.ExtendSecs,
 | 
			
		||||
		Prompt:       data.Prompt,
 | 
			
		||||
		Tags:         data.Tags,
 | 
			
		||||
		Model:        data.Model,
 | 
			
		||||
		Instrumental: data.Instrumental,
 | 
			
		||||
		SongId:       data.SongId,
 | 
			
		||||
		AudioURL:     data.AudioURL,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 插入数据库
 | 
			
		||||
	job := model.SunoJob{
 | 
			
		||||
		UserId:       task.UserId,
 | 
			
		||||
		Prompt:       data.Prompt,
 | 
			
		||||
		Instrumental: data.Instrumental,
 | 
			
		||||
		ModelName:    data.Model,
 | 
			
		||||
		TaskInfo:     utils.JsonEncode(task),
 | 
			
		||||
		Tags:         data.Tags,
 | 
			
		||||
		Title:        data.Title,
 | 
			
		||||
		Type:         data.Type,
 | 
			
		||||
		RefSongId:    data.RefSongId,
 | 
			
		||||
		RefTaskId:    data.RefTaskId,
 | 
			
		||||
		ExtendSecs:   data.ExtendSecs,
 | 
			
		||||
		Power:        h.App.SysConfig.SunoPower,
 | 
			
		||||
		SongId:       utils.RandString(32),
 | 
			
		||||
	}
 | 
			
		||||
	if data.Lyrics != "" {
 | 
			
		||||
		job.Prompt = data.Lyrics
 | 
			
		||||
	}
 | 
			
		||||
	tx := h.DB.Create(&job)
 | 
			
		||||
	if tx.Error != nil {
 | 
			
		||||
		resp.ERROR(c, tx.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建任务
 | 
			
		||||
	task.Id = job.Id
 | 
			
		||||
	h.sunoService.PushTask(task)
 | 
			
		||||
 | 
			
		||||
	// update user's power
 | 
			
		||||
	err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
 | 
			
		||||
		Type:      types.PowerConsume,
 | 
			
		||||
		Model:     job.ModelName,
 | 
			
		||||
		Remark:    fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName),
 | 
			
		||||
		CreatedAt: time.Now(),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *SunoHandler) List(c *gin.Context) {
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	page := h.GetInt(c, "page", 1)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 20)
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
 | 
			
		||||
 | 
			
		||||
	// 统计总数
 | 
			
		||||
	var total int64
 | 
			
		||||
	session.Model(&model.SunoJob{}).Count(&total)
 | 
			
		||||
 | 
			
		||||
	if page > 0 && pageSize > 0 {
 | 
			
		||||
		offset := (page - 1) * pageSize
 | 
			
		||||
		session = session.Offset(offset).Limit(pageSize)
 | 
			
		||||
	}
 | 
			
		||||
	var list []model.SunoJob
 | 
			
		||||
	err := session.Order("id desc").Find(&list).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 初始化续写关系
 | 
			
		||||
	songIds := make([]string, 0)
 | 
			
		||||
	for _, v := range list {
 | 
			
		||||
		if v.RefTaskId != "" {
 | 
			
		||||
			songIds = append(songIds, v.RefSongId)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	var tasks []model.SunoJob
 | 
			
		||||
	h.DB.Where("song_id IN ?", songIds).Find(&tasks)
 | 
			
		||||
	songMap := make(map[string]model.SunoJob)
 | 
			
		||||
	for _, t := range tasks {
 | 
			
		||||
		songMap[t.SongId] = t
 | 
			
		||||
	}
 | 
			
		||||
	// 转换为 VO
 | 
			
		||||
	items := make([]vo.SunoJob, 0)
 | 
			
		||||
	for _, v := range list {
 | 
			
		||||
		var item vo.SunoJob
 | 
			
		||||
		err = utils.CopyObject(v, &item)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		item.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
		if s, ok := songMap[v.RefSongId]; ok {
 | 
			
		||||
			item.RefSong = map[string]interface{}{
 | 
			
		||||
				"id":    s.Id,
 | 
			
		||||
				"title": s.Title,
 | 
			
		||||
				"cover": s.CoverURL,
 | 
			
		||||
				"audio": s.AudioURL,
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		items = append(items, item)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *SunoHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	var job model.SunoJob
 | 
			
		||||
	err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 只有失败或者已完成的任务可以删除
 | 
			
		||||
	if !(job.Progress == service.FailTaskProgress || job.Progress == 100) {
 | 
			
		||||
		resp.ERROR(c, "只有失败和超时(10分钟)的任务才能删除!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 删除任务
 | 
			
		||||
	err = h.DB.Delete(&job).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 删除文件
 | 
			
		||||
	_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
 | 
			
		||||
	_ = h.uploader.GetUploadHandler().Delete(job.AudioURL)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *SunoHandler) Publish(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	publish := h.GetBool(c, "publish")
 | 
			
		||||
	err := h.DB.Model(&model.SunoJob{}).Where("id", id).Where("user_id", userId).UpdateColumn("publish", publish).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *SunoHandler) Update(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id    int    `json:"id"`
 | 
			
		||||
		Title string `json:"title"`
 | 
			
		||||
		Cover string `json:"cover"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if data.Id == 0 || data.Title == "" || data.Cover == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	var item model.SunoJob
 | 
			
		||||
	if err := h.DB.Where("id", data.Id).Where("user_id", userId).First(&item).Error; err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	item.Title = data.Title
 | 
			
		||||
	item.CoverURL = data.Cover
 | 
			
		||||
 | 
			
		||||
	if err := h.DB.Updates(&item).Error; err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Detail 歌曲详情
 | 
			
		||||
func (h *SunoHandler) Detail(c *gin.Context) {
 | 
			
		||||
	songId := c.Query("song_id")
 | 
			
		||||
	if songId == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var item model.SunoJob
 | 
			
		||||
	if err := h.DB.Where("song_id", songId).First(&item).Error; err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 读取用户信息
 | 
			
		||||
	var user model.User
 | 
			
		||||
	if err := h.DB.Where("id", item.UserId).First(&user).Error; err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var itemVo vo.SunoJob
 | 
			
		||||
	if err := utils.CopyObject(item, &itemVo); err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	itemVo.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
	itemVo.User = map[string]interface{}{
 | 
			
		||||
		"nickname": user.Nickname,
 | 
			
		||||
		"avatar":   user.Avatar,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, itemVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Play 增加歌曲播放次数
 | 
			
		||||
func (h *SunoHandler) Play(c *gin.Context) {
 | 
			
		||||
	songId := c.Query("song_id")
 | 
			
		||||
	if songId == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	h.DB.Model(&model.SunoJob{}).Where("song_id", songId).UpdateColumn("play_times", gorm.Expr("play_times + ?", 1))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								api/handler/test_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								api/handler/test_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/payment"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TestHandler struct {
 | 
			
		||||
	db        *gorm.DB
 | 
			
		||||
	snowflake *service.Snowflake
 | 
			
		||||
	js        *payment.GeekPayService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.GeekPayService) *TestHandler {
 | 
			
		||||
	return &TestHandler{db: db, snowflake: snowflake, js: js}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *TestHandler) SseTest(c *gin.Context) {
 | 
			
		||||
	//c.Header("Body-Type", "text/event-stream")
 | 
			
		||||
	//c.Header("Cache-Control", "no-cache")
 | 
			
		||||
	//c.Header("Connection", "keep-alive")
 | 
			
		||||
	//
 | 
			
		||||
	//
 | 
			
		||||
	//// 模拟实时数据更新
 | 
			
		||||
	//for i := 0; i < 10; i++ {
 | 
			
		||||
	//	// 发送 SSE 数据
 | 
			
		||||
	//	_, err := fmt.Fprintf(c.Writer, "data: %v\n\n", data)
 | 
			
		||||
	//	if err != nil {
 | 
			
		||||
	//		return
 | 
			
		||||
	//	}
 | 
			
		||||
	//	c.Writer.Flush()            // 确保立即发送数据
 | 
			
		||||
	//	time.Sleep(1 * time.Second) // 每秒发送一次数据
 | 
			
		||||
	//}
 | 
			
		||||
	//c.Abort()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *TestHandler) PostTest(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Message string `json:"message"`
 | 
			
		||||
		UserId  uint   `json:"user_id"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 将参数存储在上下文中
 | 
			
		||||
	c.Set("data", data)
 | 
			
		||||
	c.Next()
 | 
			
		||||
}
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UploadHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db              *gorm.DB
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
 | 
			
		||||
	handler := &UploadHandler{db: db, uploaderManager: manager}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) Upload(c *gin.Context) {
 | 
			
		||||
	fileURL, err := h.uploaderManager.GetActiveService().PutFile(c, "file")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	resp.SUCCESS(c, fileURL)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +1,28 @@
 | 
			
		||||
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 (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
@@ -21,135 +30,246 @@ import (
 | 
			
		||||
 | 
			
		||||
type UserHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db            *gorm.DB
 | 
			
		||||
	searcher      *xdb.Searcher
 | 
			
		||||
	leveldb       *store.LevelDB
 | 
			
		||||
	uploadManager *oss.UploaderManager
 | 
			
		||||
	searcher       *xdb.Searcher
 | 
			
		||||
	redis          *redis.Client
 | 
			
		||||
	licenseService *service.LicenseService
 | 
			
		||||
	captcha        *service.CaptchaService
 | 
			
		||||
	userService    *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUserHandler(
 | 
			
		||||
	app *core.AppServer,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	searcher *xdb.Searcher,
 | 
			
		||||
	levelDB *store.LevelDB,
 | 
			
		||||
	manager *oss.UploaderManager) *UserHandler {
 | 
			
		||||
	handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, uploadManager: manager}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
	client *redis.Client,
 | 
			
		||||
	captcha *service.CaptchaService,
 | 
			
		||||
	userService *service.UserService,
 | 
			
		||||
	licenseService *service.LicenseService) *UserHandler {
 | 
			
		||||
	return &UserHandler{
 | 
			
		||||
		BaseHandler:    BaseHandler{DB: db, App: app},
 | 
			
		||||
		searcher:       searcher,
 | 
			
		||||
		redis:          client,
 | 
			
		||||
		captcha:        captcha,
 | 
			
		||||
		licenseService: licenseService,
 | 
			
		||||
		userService:    userService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Register user register
 | 
			
		||||
func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
	// parameters process
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Username string `json:"username"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
		Mobile   string `json:"mobile"`
 | 
			
		||||
		Code     int    `json:"code"`
 | 
			
		||||
		RegWay     string `json:"reg_way"`
 | 
			
		||||
		Username   string `json:"username"`
 | 
			
		||||
		Mobile     string `json:"mobile"`
 | 
			
		||||
		Email      string `json:"email"`
 | 
			
		||||
		Password   string `json:"password"`
 | 
			
		||||
		Code       string `json:"code"`
 | 
			
		||||
		InviteCode string `json:"invite_code"`
 | 
			
		||||
		Key        string `json:"key,omitempty"`
 | 
			
		||||
		Dots       string `json:"dots,omitempty"`
 | 
			
		||||
		X          int    `json:"x,omitempty"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	data.Username = strings.TrimSpace(data.Username)
 | 
			
		||||
	data.Password = strings.TrimSpace(data.Password)
 | 
			
		||||
 | 
			
		||||
	if len(data.Username) < 5 {
 | 
			
		||||
		resp.ERROR(c, "用户名长度不能少于5个字符")
 | 
			
		||||
		return
 | 
			
		||||
	if h.App.SysConfig.EnabledVerify && data.RegWay == "username" {
 | 
			
		||||
		var check bool
 | 
			
		||||
		if data.X != 0 {
 | 
			
		||||
			check = h.captcha.SlideCheck(data)
 | 
			
		||||
		} else {
 | 
			
		||||
			check = h.captcha.Check(data)
 | 
			
		||||
		}
 | 
			
		||||
		if !check {
 | 
			
		||||
			resp.ERROR(c, "请先完人机验证")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data.Password = strings.TrimSpace(data.Password)
 | 
			
		||||
	if len(data.Password) < 8 {
 | 
			
		||||
		resp.ERROR(c, "密码长度不能少于8个字符")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检测最大注册人数
 | 
			
		||||
	var 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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Mobile
 | 
			
		||||
	if h.App.SysConfig.EnabledMsgService {
 | 
			
		||||
		var code int
 | 
			
		||||
		err := h.leveldb.Get(key, &code)
 | 
			
		||||
	var key string
 | 
			
		||||
	if data.RegWay == "email" {
 | 
			
		||||
		key = CodeStorePrefix + data.Email
 | 
			
		||||
		code, err := h.redis.Get(c, key).Result()
 | 
			
		||||
		if err != nil || code != data.Code {
 | 
			
		||||
			logger.Info(code)
 | 
			
		||||
			resp.ERROR(c, "短信验证码错误")
 | 
			
		||||
			resp.ERROR(c, "验证码错误")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	} else if data.RegWay == "mobile" {
 | 
			
		||||
		key = CodeStorePrefix + data.Mobile
 | 
			
		||||
		code, err := h.redis.Get(c, key).Result()
 | 
			
		||||
		if err != nil || code != data.Code {
 | 
			
		||||
			resp.ERROR(c, "验证码错误")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if the username is exists
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.db.Where("username = ?", data.Username).First(&item)
 | 
			
		||||
	if res.RowsAffected > 0 {
 | 
			
		||||
		resp.ERROR(c, "用户名已存在")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res = h.db.Where("mobile = ?", data.Mobile).First(&item)
 | 
			
		||||
	if res.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)
 | 
			
		||||
	// 验证邀请码
 | 
			
		||||
	inviteCode := model.InviteCode{}
 | 
			
		||||
	if data.InviteCode != "" {
 | 
			
		||||
		res := h.DB.Where("code = ?", data.InviteCode).First(&inviteCode)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "无效的邀请码")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	salt := utils.RandString(8)
 | 
			
		||||
	user := model.User{
 | 
			
		||||
		Username:  data.Username,
 | 
			
		||||
		Password:  utils.GenPassword(data.Password, salt),
 | 
			
		||||
		Nickname:  fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)),
 | 
			
		||||
		Avatar:    "/images/avatar/user.png",
 | 
			
		||||
		Salt:      salt,
 | 
			
		||||
		Status:    true,
 | 
			
		||||
		Mobile:    data.Mobile,
 | 
			
		||||
		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:        "",
 | 
			
		||||
		}),
 | 
			
		||||
		Calls:    h.App.SysConfig.UserInitCalls,
 | 
			
		||||
		ImgCalls: h.App.SysConfig.InitImgCalls,
 | 
			
		||||
		ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
 | 
			
		||||
		Power:     h.App.SysConfig.InitPower,
 | 
			
		||||
	}
 | 
			
		||||
	res = h.db.Create(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "保存数据失败")
 | 
			
		||||
		logger.Error(res.Error)
 | 
			
		||||
 | 
			
		||||
	// check if the username is existing
 | 
			
		||||
	var item model.User
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	if data.Mobile != "" {
 | 
			
		||||
		session = session.Where("mobile = ?", data.Mobile)
 | 
			
		||||
		user.Username = data.Mobile
 | 
			
		||||
		user.Mobile = data.Mobile
 | 
			
		||||
	} else if data.Email != "" {
 | 
			
		||||
		session = session.Where("email = ?", data.Email)
 | 
			
		||||
		user.Username = data.Email
 | 
			
		||||
		user.Email = data.Email
 | 
			
		||||
	} else if data.Username != "" {
 | 
			
		||||
		session = session.Where("username = ?", data.Username)
 | 
			
		||||
	}
 | 
			
		||||
	session.First(&item)
 | 
			
		||||
	if item.Id > 0 {
 | 
			
		||||
		resp.ERROR(c, "该用户名已经被注册")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.App.SysConfig.EnabledMsgService {
 | 
			
		||||
		_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
 | 
			
		||||
	// 被邀请人也获得赠送算力
 | 
			
		||||
	if data.InviteCode != "" {
 | 
			
		||||
		user.Power += h.App.SysConfig.InvitePower
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, user)
 | 
			
		||||
	if h.licenseService.GetLicense().Configs.DeCopy {
 | 
			
		||||
		user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
 | 
			
		||||
	} else {
 | 
			
		||||
		user.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tx := h.DB.Begin()
 | 
			
		||||
	if err := tx.Create(&user).Error; err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 记录邀请关系
 | 
			
		||||
	if data.InviteCode != "" {
 | 
			
		||||
		// 增加邀请数量
 | 
			
		||||
		h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
 | 
			
		||||
		if h.App.SysConfig.InvitePower > 0 {
 | 
			
		||||
			err := h.userService.IncreasePower(int(inviteCode.UserId), h.App.SysConfig.InvitePower, model.PowerLog{
 | 
			
		||||
				Type:   types.PowerInvite,
 | 
			
		||||
				Model: "Invite",
 | 
			
		||||
				Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
 | 
			
		||||
			})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				tx.Rollback()
 | 
			
		||||
				resp.ERROR(c, err.Error())
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 添加邀请记录
 | 
			
		||||
		err := tx.Create(&model.InviteLog{
 | 
			
		||||
			InviterId:  inviteCode.UserId,
 | 
			
		||||
			UserId:     user.Id,
 | 
			
		||||
			Username:   user.Username,
 | 
			
		||||
			InviteCode: inviteCode.Code,
 | 
			
		||||
			Remark:     fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.InvitePower),
 | 
			
		||||
		}).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			resp.ERROR(c, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	tx.Commit()
 | 
			
		||||
 | 
			
		||||
	_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
 | 
			
		||||
	// 自动登录创建 token
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"user_id": user.Id,
 | 
			
		||||
		"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
 | 
			
		||||
	})
 | 
			
		||||
	tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Failed to generate token, "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 保存到 redis
 | 
			
		||||
	key = fmt.Sprintf("users/%d", user.Id)
 | 
			
		||||
	if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with save token: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Login 用户登录
 | 
			
		||||
func (h *UserHandler) Login(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Username string
 | 
			
		||||
		Password string
 | 
			
		||||
		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
 | 
			
		||||
	}
 | 
			
		||||
	verifyKey := fmt.Sprintf("users/verify/%s", data.Username)
 | 
			
		||||
	needVerify, err := h.redis.Get(c, verifyKey).Bool()
 | 
			
		||||
 | 
			
		||||
	if h.App.SysConfig.EnabledVerify && needVerify {
 | 
			
		||||
		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 user model.User
 | 
			
		||||
	res := h.db.Where("username = ? OR mobile = ?", data.Username, data.Username).First(&user)
 | 
			
		||||
	res := h.DB.Where("username = ?", data.Username).First(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		h.redis.Set(c, verifyKey, true, 0)
 | 
			
		||||
		resp.ERROR(c, "用户名不存在")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	password := utils.GenPassword(data.Password, user.Salt)
 | 
			
		||||
	if password != user.Password {
 | 
			
		||||
		h.redis.Set(c, verifyKey, true, 0)
 | 
			
		||||
		resp.ERROR(c, "用户名或密码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -162,81 +282,234 @@ func (h *UserHandler) Login(c *gin.Context) {
 | 
			
		||||
	// 更新最后登录时间和IP
 | 
			
		||||
	user.LastLoginIp = c.ClientIP()
 | 
			
		||||
	user.LastLoginAt = time.Now().Unix()
 | 
			
		||||
	h.db.Model(&user).Updates(user)
 | 
			
		||||
	h.DB.Model(&user).Updates(user)
 | 
			
		||||
 | 
			
		||||
	err := utils.SetLoginUser(c, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "保存会话失败")
 | 
			
		||||
		logger.Error("Error for save session: ", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.db.Create(&model.UserLoginLog{
 | 
			
		||||
	h.DB.Create(&model.UserLoginLog{
 | 
			
		||||
		UserId:       user.Id,
 | 
			
		||||
		Username:     user.Username,
 | 
			
		||||
		LoginIp:      c.ClientIP(),
 | 
			
		||||
		LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
	// 创建 token
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"user_id": user.Id,
 | 
			
		||||
		"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
 | 
			
		||||
	})
 | 
			
		||||
	tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Failed to generate token, "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 保存到 redis
 | 
			
		||||
	sessionKey := fmt.Sprintf("users/%d", user.Id)
 | 
			
		||||
	if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with save token: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 移除登录行为验证码
 | 
			
		||||
	h.redis.Del(c, verifyKey)
 | 
			
		||||
	resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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()
 | 
			
		||||
	key := h.GetUserKey(c)
 | 
			
		||||
	if _, err := h.redis.Del(c, key).Result(); err != nil {
 | 
			
		||||
		logger.Error("error with delete session: ", err)
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CLogin 第三方登录请求二维码
 | 
			
		||||
func (h *UserHandler) CLogin(c *gin.Context) {
 | 
			
		||||
	returnURL := h.GetTrim(c, "return_url")
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/api/clogin/request", h.App.Config.ApiConfig.ApiURL)
 | 
			
		||||
	r, err := req.C().R().SetBody(gin.H{"login_type": "wx", "return_url": returnURL}).
 | 
			
		||||
		SetHeader("AppId", h.App.Config.ApiConfig.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, "error with login http status: "+r.Status)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		resp.ERROR(c, "error with http response: "+res.Message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, res.Data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CLoginCallback 第三方登录回调
 | 
			
		||||
func (h *UserHandler) CLoginCallback(c *gin.Context) {
 | 
			
		||||
	loginType := c.Query("login_type")
 | 
			
		||||
	code := c.Query("code")
 | 
			
		||||
	userId := h.GetInt(c, "user_id", 0)
 | 
			
		||||
	action := c.Query("action")
 | 
			
		||||
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL)
 | 
			
		||||
	r, err := req.C().R().SetBody(gin.H{"login_type": loginType, "code": code}).
 | 
			
		||||
		SetHeader("AppId", h.App.Config.ApiConfig.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, "error with login http status: "+r.Status)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		resp.ERROR(c, "error with http response: "+res.Message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// login successfully
 | 
			
		||||
	data := res.Data.(map[string]interface{})
 | 
			
		||||
	var user model.User
 | 
			
		||||
	if action == "bind" && userId > 0 {
 | 
			
		||||
		err = h.DB.Where("openid", data["openid"]).First(&user).Error
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			resp.ERROR(c, "该微信已经绑定其他账号,请先解绑")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = h.DB.Where("id", userId).First(&user).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "绑定用户不存在")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "更新用户信息失败,"+err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resp.SUCCESS(c, gin.H{"token": ""})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session := gin.H{}
 | 
			
		||||
	tx := h.DB.Where("openid", data["openid"]).First(&user)
 | 
			
		||||
	if tx.Error != nil {
 | 
			
		||||
		// create new user
 | 
			
		||||
		var totalUser int64
 | 
			
		||||
		h.DB.Model(&model.User{}).Count(&totalUser)
 | 
			
		||||
		if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
 | 
			
		||||
			resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		salt := utils.RandString(8)
 | 
			
		||||
		password := fmt.Sprintf("%d", utils.RandomNumber(8))
 | 
			
		||||
		user = model.User{
 | 
			
		||||
			Username:  fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)),
 | 
			
		||||
			Password:  utils.GenPassword(password, salt),
 | 
			
		||||
			Avatar:    fmt.Sprintf("%s", data["avatar"]),
 | 
			
		||||
			Salt:      salt,
 | 
			
		||||
			Status:    true,
 | 
			
		||||
			ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
 | 
			
		||||
			Power:     h.App.SysConfig.InitPower,
 | 
			
		||||
			OpenId:    fmt.Sprintf("%s", data["openid"]),
 | 
			
		||||
			Nickname:  fmt.Sprintf("%s", data["nickname"]),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tx = h.DB.Create(&user)
 | 
			
		||||
		if tx.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "保存数据失败")
 | 
			
		||||
			logger.Error(tx.Error)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		session["username"] = user.Username
 | 
			
		||||
		session["password"] = password
 | 
			
		||||
	} else { // login directly
 | 
			
		||||
		// 更新最后登录时间和IP
 | 
			
		||||
		user.LastLoginIp = c.ClientIP()
 | 
			
		||||
		user.LastLoginAt = time.Now().Unix()
 | 
			
		||||
		h.DB.Model(&user).Updates(user)
 | 
			
		||||
 | 
			
		||||
		h.DB.Create(&model.UserLoginLog{
 | 
			
		||||
			UserId:       user.Id,
 | 
			
		||||
			Username:     user.Username,
 | 
			
		||||
			LoginIp:      c.ClientIP(),
 | 
			
		||||
			LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建 token
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"user_id": user.Id,
 | 
			
		||||
		"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
 | 
			
		||||
	})
 | 
			
		||||
	tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Failed to generate token, "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 保存到 redis
 | 
			
		||||
	key := fmt.Sprintf("users/%d", user.Id)
 | 
			
		||||
	if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with save token: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	session["token"] = tokenString
 | 
			
		||||
	resp.SUCCESS(c, session)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Session 获取/验证会话
 | 
			
		||||
func (h *UserHandler) Session(c *gin.Context) {
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		var userVo vo.User
 | 
			
		||||
		err := utils.CopyObject(user, &userVo)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c)
 | 
			
		||||
		}
 | 
			
		||||
		userVo.Id = user.Id
 | 
			
		||||
		resp.SUCCESS(c, userVo)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var userVo vo.User
 | 
			
		||||
	err = utils.CopyObject(user, &userVo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 用户 VIP 到期
 | 
			
		||||
	if user.ExpiredTime > 0 && user.ExpiredTime < time.Now().Unix() {
 | 
			
		||||
		h.DB.Model(&user).UpdateColumn("vip", false)
 | 
			
		||||
	}
 | 
			
		||||
	userVo.Id = user.Id
 | 
			
		||||
	resp.SUCCESS(c, userVo)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type userProfile struct {
 | 
			
		||||
	Id         uint             `json:"id"`
 | 
			
		||||
	Username   string           `json:"username"`
 | 
			
		||||
	Nickname   string           `json:"nickname"`
 | 
			
		||||
	Mobile     string           `json:"mobile"`
 | 
			
		||||
	Avatar     string           `json:"avatar"`
 | 
			
		||||
	ChatConfig types.ChatConfig `json:"chat_config"`
 | 
			
		||||
	Calls      int              `json:"calls"`
 | 
			
		||||
	ImgCalls   int              `json:"img_calls"`
 | 
			
		||||
	Tokens     int64            `json:"tokens"`
 | 
			
		||||
	Id          uint   `json:"id"`
 | 
			
		||||
	Nickname    string `json:"nickname"`
 | 
			
		||||
	Username    string `json:"username"`
 | 
			
		||||
	Avatar      string `json:"avatar"`
 | 
			
		||||
	Power       int    `json:"power"`
 | 
			
		||||
	ExpiredTime int64  `json:"expired_time"`
 | 
			
		||||
	Vip         bool   `json:"vip"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) Profile(c *gin.Context) {
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.db.First(&user, user.Id)
 | 
			
		||||
	h.DB.First(&user, user.Id)
 | 
			
		||||
	var profile userProfile
 | 
			
		||||
	err = utils.CopyObject(user, &profile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -256,49 +529,25 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	h.db.First(&user, user.Id)
 | 
			
		||||
	user.Nickname = data.Nickname
 | 
			
		||||
	oldAvatar := user.Avatar
 | 
			
		||||
	h.DB.First(&user, user.Id)
 | 
			
		||||
	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)
 | 
			
		||||
	user.Nickname = data.Nickname
 | 
			
		||||
	res := h.DB.Updates(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新用户信息失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// remove the old avatar image file
 | 
			
		||||
	if oldAvatar != data.Avatar {
 | 
			
		||||
		err = h.uploadManager.GetActiveService().Delete(oldAvatar)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("error with delete image: ", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Password 更新密码
 | 
			
		||||
func (h *UserHandler) Password(c *gin.Context) {
 | 
			
		||||
// UpdatePass 更新密码
 | 
			
		||||
func (h *UserHandler) UpdatePass(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		OldPass  string `json:"old_pass"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
@@ -313,70 +562,153 @@ func (h *UserHandler) Password(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	password := utils.GenPassword(data.OldPass, user.Salt)
 | 
			
		||||
	logger.Info(user.Salt, ",", user.Password, ",", password, ",", data.OldPass)
 | 
			
		||||
	logger.Debugf(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, "更新数据库失败")
 | 
			
		||||
	err = h.DB.Model(&user).UpdateColumn("password", newPass).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BindMobile 绑定手机号
 | 
			
		||||
func (h *UserHandler) BindMobile(c *gin.Context) {
 | 
			
		||||
// ResetPass 找回密码
 | 
			
		||||
func (h *UserHandler) ResetPass(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Code   int    `json:"code"`
 | 
			
		||||
		Type     string `json:"type"`     // 验证类别:mobile, email
 | 
			
		||||
		Mobile   string `json:"mobile"`   // 手机号
 | 
			
		||||
		Email    string `json:"email"`    // 邮箱地址
 | 
			
		||||
		Code     string `json:"code"`     // 验证码
 | 
			
		||||
		Password string `json:"password"` // 新密码
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查手机号是否被其他账号绑定
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.db.Where("mobile = ?", data.Mobile).First(&item)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.ERROR(c, "该手机号已经被其他账号绑定")
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{})
 | 
			
		||||
	var key string
 | 
			
		||||
	if data.Type == "email" {
 | 
			
		||||
		session = session.Where("email", data.Email)
 | 
			
		||||
		key = CodeStorePrefix + data.Email
 | 
			
		||||
	} else if data.Type == "mobile" {
 | 
			
		||||
		session = session.Where("mobile", data.Mobile)
 | 
			
		||||
		key = CodeStorePrefix + data.Mobile
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.ERROR(c, "验证类别错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var user model.User
 | 
			
		||||
	err := session.First(&user).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "用户不存在!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	code, err := h.redis.Get(c, key).Result()
 | 
			
		||||
	if err != nil || code != data.Code {
 | 
			
		||||
		resp.ERROR(c, "验证码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	password := utils.GenPassword(data.Password, user.Salt)
 | 
			
		||||
	err = h.DB.Model(&user).UpdateColumn("password", password).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
	} else {
 | 
			
		||||
		h.redis.Del(c, key)
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BindMobile 绑定手机号
 | 
			
		||||
func (h *UserHandler) BindMobile(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Code   string `json:"code"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Mobile
 | 
			
		||||
	var code int
 | 
			
		||||
	err := h.leveldb.Get(key, &code)
 | 
			
		||||
	code, err := h.redis.Get(c, key).Result()
 | 
			
		||||
	if err != nil || code != data.Code {
 | 
			
		||||
		resp.ERROR(c, "短信验证码错误")
 | 
			
		||||
		resp.ERROR(c, "验证码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	// 检查手机号是否被其他账号绑定
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.DB.Where("mobile", data.Mobile).First(&item)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.ERROR(c, "该手机号已经绑定了其他账号,请更换手机号")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
 | 
			
		||||
	err = h.DB.Model(&item).Where("id", userId).UpdateColumn("mobile", data.Mobile).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = h.leveldb.Delete(key) // 删除短信验证码
 | 
			
		||||
	_ = h.redis.Del(c, key) // 删除短信验证码
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BindEmail 绑定邮箱
 | 
			
		||||
func (h *UserHandler) BindEmail(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Email string `json:"email"`
 | 
			
		||||
		Code  string `json:"code"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Email
 | 
			
		||||
	code, err := h.redis.Get(c, key).Result()
 | 
			
		||||
	if err != nil || code != data.Code {
 | 
			
		||||
		resp.ERROR(c, "验证码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查手机号是否被其他账号绑定
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.DB.Where("email", data.Email).First(&item)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.ERROR(c, "该邮箱地址已经绑定了其他账号,请更邮箱地址")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
 | 
			
		||||
	err = h.DB.Model(&item).Where("id", userId).UpdateColumn("email", data.Email).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = h.redis.Del(c, key) // 删除短信验证码
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										215
									
								
								api/handler/video_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								api/handler/video_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,215 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/oss"
 | 
			
		||||
	"geekai/service/video"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/store/vo"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type VideoHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	videoService *video.Service
 | 
			
		||||
	uploader     *oss.UploaderManager
 | 
			
		||||
	userService  *service.UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService) *VideoHandler {
 | 
			
		||||
	return &VideoHandler{
 | 
			
		||||
		BaseHandler: BaseHandler{
 | 
			
		||||
			App: app,
 | 
			
		||||
			DB:  db,
 | 
			
		||||
		},
 | 
			
		||||
		videoService: service,
 | 
			
		||||
		uploader:     uploader,
 | 
			
		||||
		userService:  userService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *VideoHandler) LumaCreate(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	var data struct {
 | 
			
		||||
		ClientId      string `json:"client_id"`
 | 
			
		||||
		Prompt        string `json:"prompt"`
 | 
			
		||||
		FirstFrameImg string `json:"first_frame_img,omitempty"`
 | 
			
		||||
		EndFrameImg   string `json:"end_frame_img,omitempty"`
 | 
			
		||||
		ExpandPrompt  bool   `json:"expand_prompt,omitempty"`
 | 
			
		||||
		Loop          bool   `json:"loop,omitempty"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.Power < h.App.SysConfig.LumaPower {
 | 
			
		||||
		resp.ERROR(c, "您的算力不足,请充值后再试!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if data.Prompt == "" {
 | 
			
		||||
		resp.ERROR(c, "prompt is needed")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userId := int(h.GetLoginUserId(c))
 | 
			
		||||
	params := types.VideoParams{
 | 
			
		||||
		PromptOptimize: data.ExpandPrompt,
 | 
			
		||||
		Loop:           data.Loop,
 | 
			
		||||
		StartImgURL:    data.FirstFrameImg,
 | 
			
		||||
		EndImgURL:      data.EndFrameImg,
 | 
			
		||||
	}
 | 
			
		||||
	task := types.VideoTask{
 | 
			
		||||
		ClientId:         data.ClientId,
 | 
			
		||||
		UserId:           userId,
 | 
			
		||||
		Type:             types.VideoLuma,
 | 
			
		||||
		Prompt:           data.Prompt,
 | 
			
		||||
		Params:           params,
 | 
			
		||||
		TranslateModelId: h.App.SysConfig.TranslateModelId,
 | 
			
		||||
	}
 | 
			
		||||
	// 插入数据库
 | 
			
		||||
	job := model.VideoJob{
 | 
			
		||||
		UserId:   userId,
 | 
			
		||||
		Type:     types.VideoLuma,
 | 
			
		||||
		Prompt:   data.Prompt,
 | 
			
		||||
		Power:    h.App.SysConfig.LumaPower,
 | 
			
		||||
		TaskInfo: utils.JsonEncode(task),
 | 
			
		||||
	}
 | 
			
		||||
	tx := h.DB.Create(&job)
 | 
			
		||||
	if tx.Error != nil {
 | 
			
		||||
		resp.ERROR(c, tx.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建任务
 | 
			
		||||
	task.Id = job.Id
 | 
			
		||||
	h.videoService.PushTask(task)
 | 
			
		||||
 | 
			
		||||
	// update user's power
 | 
			
		||||
	err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
 | 
			
		||||
		Type:   types.PowerConsume,
 | 
			
		||||
		Model:  "luma",
 | 
			
		||||
		Remark: fmt.Sprintf("Luma 文生视频,任务ID:%d", job.Id),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *VideoHandler) List(c *gin.Context) {
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	t := c.Query("type")
 | 
			
		||||
	page := h.GetInt(c, "page", 1)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 20)
 | 
			
		||||
	all := h.GetBool(c, "all")
 | 
			
		||||
	session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
 | 
			
		||||
	if t != "" {
 | 
			
		||||
		session = session.Where("type", t)
 | 
			
		||||
	}
 | 
			
		||||
	if all {
 | 
			
		||||
		session = session.Where("publish", 0).Where("progress", 100)
 | 
			
		||||
	} else {
 | 
			
		||||
		session = session.Where("user_id", h.GetLoginUserId(c))
 | 
			
		||||
	}
 | 
			
		||||
	// 统计总数
 | 
			
		||||
	var total int64
 | 
			
		||||
	session.Model(&model.VideoJob{}).Count(&total)
 | 
			
		||||
 | 
			
		||||
	if page > 0 && pageSize > 0 {
 | 
			
		||||
		offset := (page - 1) * pageSize
 | 
			
		||||
		session = session.Offset(offset).Limit(pageSize)
 | 
			
		||||
	}
 | 
			
		||||
	var list []model.VideoJob
 | 
			
		||||
	err := session.Order("id desc").Find(&list).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 转换为 VO
 | 
			
		||||
	items := make([]vo.VideoJob, 0)
 | 
			
		||||
	for _, v := range list {
 | 
			
		||||
		var item vo.VideoJob
 | 
			
		||||
		err = utils.CopyObject(v, &item)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		item.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
		if item.VideoURL == "" {
 | 
			
		||||
			item.VideoURL = v.WaterURL
 | 
			
		||||
		}
 | 
			
		||||
		items = append(items, item)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *VideoHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	var job model.VideoJob
 | 
			
		||||
	err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 只有失败或者超时的任务才能删除
 | 
			
		||||
	if !(job.Progress == service.FailTaskProgress || time.Now().After(job.CreatedAt.Add(time.Minute*30))) {
 | 
			
		||||
		resp.ERROR(c, "只有失败和超时(30分钟)的任务才能删除!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 删除任务
 | 
			
		||||
	err = h.DB.Delete(&job).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 删除文件
 | 
			
		||||
	_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
 | 
			
		||||
	_ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *VideoHandler) Publish(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	publish := h.GetBool(c, "publish")
 | 
			
		||||
	var job model.VideoJob
 | 
			
		||||
	err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = h.DB.Model(&job).UpdateColumn("publish", publish).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										151
									
								
								api/handler/ws_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								api/handler/ws_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Websocket 连接处理 handler
 | 
			
		||||
 | 
			
		||||
type WebsocketHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	wsService   *service.WebsocketService
 | 
			
		||||
	chatHandler *ChatHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewWebsocketHandler(app *core.AppServer, s *service.WebsocketService, db *gorm.DB, chatHandler *ChatHandler) *WebsocketHandler {
 | 
			
		||||
	return &WebsocketHandler{
 | 
			
		||||
		BaseHandler: BaseHandler{App: app, DB: db},
 | 
			
		||||
		chatHandler: chatHandler,
 | 
			
		||||
		wsService:   s,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *WebsocketHandler) Client(c *gin.Context) {
 | 
			
		||||
	clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
 | 
			
		||||
	ws, err := (&websocket.Upgrader{
 | 
			
		||||
		CheckOrigin:  func(r *http.Request) bool { return true },
 | 
			
		||||
		Subprotocols: strings.Split(clientProtocols, ","),
 | 
			
		||||
	}).Upgrade(c.Writer, c.Request, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	clientId := c.Query("client_id")
 | 
			
		||||
	client := types.NewWsClient(ws, clientId)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	if userId == 0 {
 | 
			
		||||
		_ = client.Send([]byte("Invalid user_id"))
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var user model.User
 | 
			
		||||
	if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
 | 
			
		||||
		_ = client.Send([]byte("Invalid user_id"))
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.wsService.Clients.Put(clientId, client)
 | 
			
		||||
	logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			_, msg, err := client.Receive()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
 | 
			
		||||
				client.Close()
 | 
			
		||||
				h.wsService.Clients.Delete(clientId)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var message types.InputMessage
 | 
			
		||||
			err = utils.JsonDecode(string(msg), &message)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			logger.Debugf("Receive a message:%+v", message)
 | 
			
		||||
			if message.Type == types.MsgTypePing {
 | 
			
		||||
				utils.SendChannelMsg(client, types.ChPing, "pong")
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 当前只处理聊天消息,其他消息全部丢弃
 | 
			
		||||
			var chatMessage types.ChatMessage
 | 
			
		||||
			err = utils.JsonDecode(utils.JsonEncode(message.Body), &chatMessage)
 | 
			
		||||
			if err != nil || message.Channel != types.ChChat {
 | 
			
		||||
				logger.Warnf("invalid message body:%+v", message.Body)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			var chatRole model.ChatRole
 | 
			
		||||
			err = h.DB.First(&chatRole, chatMessage.RoleId).Error
 | 
			
		||||
			if err != nil || !chatRole.Enable {
 | 
			
		||||
				utils.SendAndFlush(client, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!!!")
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			// if the role bind a model_id, use role's bind model_id
 | 
			
		||||
			if chatRole.ModelId > 0 {
 | 
			
		||||
				chatMessage.RoleId = chatRole.ModelId
 | 
			
		||||
			}
 | 
			
		||||
			// get model info
 | 
			
		||||
			var chatModel model.ChatModel
 | 
			
		||||
			err = h.DB.Where("id", chatMessage.ModelId).First(&chatModel).Error
 | 
			
		||||
			if err != nil || chatModel.Enabled == false {
 | 
			
		||||
				utils.SendAndFlush(client, "当前AI模型暂未启用,请更换模型后再发起对话!!!")
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			session := &types.ChatSession{
 | 
			
		||||
				ClientIP: c.ClientIP(),
 | 
			
		||||
				UserId:   userId,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// use old chat data override the chat model and role ID
 | 
			
		||||
			var chat model.ChatItem
 | 
			
		||||
			h.DB.Where("chat_id", chatMessage.ChatId).First(&chat)
 | 
			
		||||
			if chat.Id > 0 {
 | 
			
		||||
				chatModel.Id = chat.ModelId
 | 
			
		||||
				chatMessage.RoleId = int(chat.RoleId)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			session.ChatId = chatMessage.ChatId
 | 
			
		||||
			session.Tools = chatMessage.Tools
 | 
			
		||||
			session.Stream = chatMessage.Stream
 | 
			
		||||
			// 复制模型数据
 | 
			
		||||
			err = utils.CopyObject(chatModel, &session.Model)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err, chatModel)
 | 
			
		||||
			}
 | 
			
		||||
			session.Model.Id = chatModel.Id
 | 
			
		||||
			ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
			h.chatHandler.ReqCancelFunc.Put(clientId, cancel)
 | 
			
		||||
			err = h.chatHandler.sendMessage(ctx, session, chatRole, chatMessage.Content, client)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
				utils.SendAndFlush(client, err.Error())
 | 
			
		||||
			} else {
 | 
			
		||||
				utils.SendMsg(client, types.ReplyMessage{Channel: types.ChChat, Type: types.MsgTypeEnd})
 | 
			
		||||
				logger.Infof("回答完毕: %v", message.Body)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,12 @@
 | 
			
		||||
package logger
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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 (
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
	"go.uber.org/zap/zapcore"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										439
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						
									
										439
									
								
								api/main.go
									
									
									
									
									
								
							@@ -1,17 +1,30 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/handler/admin"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/service/function"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"context"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/handler"
 | 
			
		||||
	"geekai/handler/admin"
 | 
			
		||||
	logger2 "geekai/logger"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/dalle"
 | 
			
		||||
	"geekai/service/mj"
 | 
			
		||||
	"geekai/service/oss"
 | 
			
		||||
	"geekai/service/payment"
 | 
			
		||||
	"geekai/service/sd"
 | 
			
		||||
	"geekai/service/sms"
 | 
			
		||||
	"geekai/service/suno"
 | 
			
		||||
	"geekai/service/video"
 | 
			
		||||
	"geekai/store"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -20,6 +33,8 @@ import (
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
@@ -27,7 +42,7 @@ import (
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
//go:embed res/ip2region.xdb
 | 
			
		||||
//go:embed res
 | 
			
		||||
var xdbFS embed.FS
 | 
			
		||||
 | 
			
		||||
// AppLifecycle 应用程序生命周期
 | 
			
		||||
@@ -36,34 +51,34 @@ type AppLifecycle struct {
 | 
			
		||||
 | 
			
		||||
// OnStart 应用程序启动时执行
 | 
			
		||||
func (l *AppLifecycle) OnStart(context.Context) error {
 | 
			
		||||
	log.Println("AppLifecycle OnStart")
 | 
			
		||||
	logger.Info("AppLifecycle OnStart")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OnStop 应用程序停止时执行
 | 
			
		||||
func (l *AppLifecycle) OnStop(context.Context) error {
 | 
			
		||||
	log.Println("AppLifecycle OnStop")
 | 
			
		||||
	logger.Info("AppLifecycle OnStop")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAppLifeCycle() *AppLifecycle {
 | 
			
		||||
	return &AppLifecycle{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	configFile := os.Getenv("CONFIG_FILE")
 | 
			
		||||
	if configFile == "" {
 | 
			
		||||
		configFile = "config.toml"
 | 
			
		||||
	}
 | 
			
		||||
	var debug bool
 | 
			
		||||
	debugEnv := os.Getenv("DEBUG")
 | 
			
		||||
	if debugEnv == "" {
 | 
			
		||||
		debug = true
 | 
			
		||||
	} else {
 | 
			
		||||
		debug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
 | 
			
		||||
	}
 | 
			
		||||
	debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG"))
 | 
			
		||||
	logger.Info("Loading config file: ", configFile)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := recover(); err != nil {
 | 
			
		||||
			logger.Error("Panic Error:", err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	if !debug {
 | 
			
		||||
		defer func() {
 | 
			
		||||
			if err := recover(); err != nil {
 | 
			
		||||
				logger.Error("Panic Error:", err)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	app := fx.New(
 | 
			
		||||
		// 初始化配置应用配置
 | 
			
		||||
@@ -81,15 +96,20 @@ func main() {
 | 
			
		||||
		// 创建应用服务
 | 
			
		||||
		fx.Provide(core.NewServer),
 | 
			
		||||
		// 初始化
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer) {
 | 
			
		||||
			s.Init(debug)
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, client *redis.Client) {
 | 
			
		||||
			s.Init(debug, client)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 初始化数据库
 | 
			
		||||
		fx.Provide(store.NewGormConfig),
 | 
			
		||||
		fx.Provide(store.NewMysql),
 | 
			
		||||
		fx.Provide(store.NewRedisClient),
 | 
			
		||||
		fx.Provide(store.NewLevelDB),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(func() embed.FS {
 | 
			
		||||
			return xdbFS
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 创建 Ip2Region 查询对象
 | 
			
		||||
		fx.Provide(func() (*xdb.Searcher, error) {
 | 
			
		||||
			file, err := xdbFS.Open("res/ip2region.xdb")
 | 
			
		||||
@@ -104,38 +124,111 @@ func main() {
 | 
			
		||||
			return xdb.NewWithBuffer(cBuff)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 创建函数
 | 
			
		||||
		fx.Provide(function.NewFunctions),
 | 
			
		||||
 | 
			
		||||
		// 创建控制器
 | 
			
		||||
		fx.Provide(handler.NewChatRoleHandler),
 | 
			
		||||
		fx.Provide(handler.NewUserHandler),
 | 
			
		||||
		fx.Provide(handler.NewChatHandler),
 | 
			
		||||
		fx.Provide(handler.NewUploadHandler),
 | 
			
		||||
		fx.Provide(handler.NewNetHandler),
 | 
			
		||||
		fx.Provide(handler.NewSmsHandler),
 | 
			
		||||
		fx.Provide(handler.NewRewardHandler),
 | 
			
		||||
		fx.Provide(handler.NewRedeemHandler),
 | 
			
		||||
		fx.Provide(handler.NewCaptchaHandler),
 | 
			
		||||
		fx.Provide(handler.NewMidJourneyHandler),
 | 
			
		||||
		fx.Provide(handler.NewChatModelHandler),
 | 
			
		||||
		fx.Provide(handler.NewSdJobHandler),
 | 
			
		||||
		fx.Provide(handler.NewPaymentHandler),
 | 
			
		||||
		fx.Provide(handler.NewOrderHandler),
 | 
			
		||||
		fx.Provide(handler.NewProductHandler),
 | 
			
		||||
		fx.Provide(handler.NewConfigHandler),
 | 
			
		||||
		fx.Provide(handler.NewPowerLogHandler),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(admin.NewConfigHandler),
 | 
			
		||||
		fx.Provide(admin.NewAdminHandler),
 | 
			
		||||
		fx.Provide(admin.NewApiKeyHandler),
 | 
			
		||||
		fx.Provide(admin.NewUserHandler),
 | 
			
		||||
		fx.Provide(admin.NewChatRoleHandler),
 | 
			
		||||
		fx.Provide(admin.NewRewardHandler),
 | 
			
		||||
		fx.Provide(admin.NewChatAppHandler),
 | 
			
		||||
		fx.Provide(admin.NewRedeemHandler),
 | 
			
		||||
		fx.Provide(admin.NewDashboardHandler),
 | 
			
		||||
		fx.Provide(admin.NewChatModelHandler),
 | 
			
		||||
		fx.Provide(admin.NewProductHandler),
 | 
			
		||||
		fx.Provide(admin.NewOrderHandler),
 | 
			
		||||
		fx.Provide(admin.NewChatHandler),
 | 
			
		||||
		fx.Provide(admin.NewPowerLogHandler),
 | 
			
		||||
 | 
			
		||||
		// 创建服务
 | 
			
		||||
		fx.Provide(service.NewAliYunSmsService),
 | 
			
		||||
		fx.Provide(sms.NewSendServiceManager),
 | 
			
		||||
		fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
 | 
			
		||||
			return service.NewCaptchaService(config.ApiConfig)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(oss.NewUploaderManager),
 | 
			
		||||
		fx.Provide(dalle.NewService),
 | 
			
		||||
		fx.Invoke(func(s *dalle.Service) {
 | 
			
		||||
			s.Run()
 | 
			
		||||
			s.CheckTaskNotify()
 | 
			
		||||
			s.DownloadImages()
 | 
			
		||||
			s.CheckTaskStatus()
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 邮件服务
 | 
			
		||||
		fx.Provide(service.NewSmtpService),
 | 
			
		||||
		// License 服务
 | 
			
		||||
		fx.Provide(service.NewLicenseService),
 | 
			
		||||
		fx.Invoke(func(licenseService *service.LicenseService) {
 | 
			
		||||
			// licenseService.SyncLicense()
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// MidJourney service pool
 | 
			
		||||
		fx.Provide(mj.NewService),
 | 
			
		||||
		fx.Provide(mj.NewClient),
 | 
			
		||||
		fx.Invoke(func(s *mj.Service) {
 | 
			
		||||
			s.Run()
 | 
			
		||||
			s.SyncTaskProgress()
 | 
			
		||||
			s.CheckTaskNotify()
 | 
			
		||||
			s.DownloadImages()
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// Stable Diffusion 机器人
 | 
			
		||||
		fx.Provide(sd.NewService),
 | 
			
		||||
		fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
 | 
			
		||||
			s.Run()
 | 
			
		||||
			s.CheckTaskStatus()
 | 
			
		||||
			s.CheckTaskNotify()
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(suno.NewService),
 | 
			
		||||
		fx.Invoke(func(s *suno.Service) {
 | 
			
		||||
			s.Run()
 | 
			
		||||
			s.SyncTaskProgress()
 | 
			
		||||
			s.CheckTaskNotify()
 | 
			
		||||
			s.DownloadFiles()
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(video.NewService),
 | 
			
		||||
		fx.Invoke(func(s *video.Service) {
 | 
			
		||||
			s.Run()
 | 
			
		||||
			s.SyncTaskProgress()
 | 
			
		||||
			s.CheckTaskNotify()
 | 
			
		||||
			s.DownloadFiles()
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(service.NewUserService),
 | 
			
		||||
		fx.Provide(payment.NewAlipayService),
 | 
			
		||||
		fx.Provide(payment.NewHuPiPay),
 | 
			
		||||
		fx.Provide(payment.NewJPayService),
 | 
			
		||||
		fx.Provide(payment.NewWechatService),
 | 
			
		||||
		fx.Provide(service.NewSnowflake),
 | 
			
		||||
		fx.Provide(service.NewXXLJobExecutor),
 | 
			
		||||
		fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
 | 
			
		||||
			if config.XXLConfig.Enabled {
 | 
			
		||||
				go func() {
 | 
			
		||||
					log.Fatal(exec.Run())
 | 
			
		||||
				}()
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 注册路由
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/role/")
 | 
			
		||||
			group := s.Engine.Group("/api/app/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("list/user", h.ListByUser)
 | 
			
		||||
			group.POST("update", h.UpdateRole)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/user/")
 | 
			
		||||
@@ -145,62 +238,94 @@ func main() {
 | 
			
		||||
			group.GET("session", h.Session)
 | 
			
		||||
			group.GET("profile", h.Profile)
 | 
			
		||||
			group.POST("profile/update", h.ProfileUpdate)
 | 
			
		||||
			group.POST("password", h.Password)
 | 
			
		||||
			group.POST("password", h.UpdatePass)
 | 
			
		||||
			group.POST("bind/mobile", h.BindMobile)
 | 
			
		||||
			group.POST("bind/email", h.BindEmail)
 | 
			
		||||
			group.POST("resetPass", h.ResetPass)
 | 
			
		||||
			group.GET("clogin", h.CLogin)
 | 
			
		||||
			group.GET("clogin/callback", h.CLoginCallback)
 | 
			
		||||
		}),
 | 
			
		||||
		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.GET("detail", h.Detail)
 | 
			
		||||
			group.POST("update", h.Update)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("history", h.History)
 | 
			
		||||
			group.GET("clear", h.Clear)
 | 
			
		||||
			group.GET("tokens", h.Tokens)
 | 
			
		||||
			group.POST("tokens", h.Tokens)
 | 
			
		||||
			group.GET("stop", h.StopGenerate)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
 | 
			
		||||
			s.Engine.POST("/api/upload", h.Upload)
 | 
			
		||||
			s.Engine.POST("/api/upload/list", h.List)
 | 
			
		||||
			s.Engine.GET("/api/upload/remove", h.Remove)
 | 
			
		||||
			s.Engine.GET("/api/download", h.Download)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/sms/")
 | 
			
		||||
			group.GET("status", h.Status)
 | 
			
		||||
			group.POST("code", h.SendCode)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/captcha/")
 | 
			
		||||
			group.GET("get", h.Get)
 | 
			
		||||
			group.POST("check", h.Check)
 | 
			
		||||
			group.GET("slide/get", h.SlideGet)
 | 
			
		||||
			group.POST("slide/check", h.SlideCheck)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/reward/")
 | 
			
		||||
			group.POST("notify", h.Notify)
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/redeem/")
 | 
			
		||||
			group.POST("verify", h.Verify)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
 | 
			
		||||
			s.Engine.POST("/api/mj/notify", h.Notify)
 | 
			
		||||
			s.Engine.POST("/api/mj/upscale", h.Upscale)
 | 
			
		||||
			s.Engine.POST("/api/mj/variation", h.Variation)
 | 
			
		||||
			group := s.Engine.Group("/api/mj/")
 | 
			
		||||
			group.POST("image", h.Image)
 | 
			
		||||
			group.POST("upscale", h.Upscale)
 | 
			
		||||
			group.POST("variation", h.Variation)
 | 
			
		||||
			group.GET("jobs", h.JobList)
 | 
			
		||||
			group.GET("imgWall", h.ImgWall)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("publish", h.Publish)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/sd")
 | 
			
		||||
			group.POST("image", h.Image)
 | 
			
		||||
			group.GET("jobs", h.JobList)
 | 
			
		||||
			group.GET("imgWall", h.ImgWall)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("publish", h.Publish)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/config/")
 | 
			
		||||
			group.GET("get", h.Get)
 | 
			
		||||
			group.GET("license", h.License)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 管理后台控制器
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/config/")
 | 
			
		||||
			group := s.Engine.Group("/api/admin/config")
 | 
			
		||||
			group.POST("update", h.Update)
 | 
			
		||||
			group.GET("get", h.Get)
 | 
			
		||||
			group.POST("active", h.Active)
 | 
			
		||||
			group.GET("fixData", h.FixData)
 | 
			
		||||
			group.GET("license", h.GetLicense)
 | 
			
		||||
		}),
 | 
			
		||||
		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)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.POST("enable", h.Enable)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.POST("resetPass", h.ResetPass)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/apikey/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
 | 
			
		||||
@@ -211,29 +336,207 @@ func main() {
 | 
			
		||||
			group.GET("loginLog", h.LoginLog)
 | 
			
		||||
			group.POST("resetPass", h.ResetPass)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) {
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/role/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.POST("sort", h.SetSort)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/reward/")
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/redeem/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("create", h.Create)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.POST("export", h.Export)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/dashboard/")
 | 
			
		||||
			group.GET("stats", h.Stats)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
 | 
			
		||||
			err := s.Run(db)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/model/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/model/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/payment/")
 | 
			
		||||
			group.POST("doPay", h.Pay)
 | 
			
		||||
			group.GET("payWays", h.GetPayWays)
 | 
			
		||||
			group.POST("notify/alipay", h.AlipayNotify)
 | 
			
		||||
			group.GET("notify/geek", h.GeekPayNotify)
 | 
			
		||||
			group.POST("notify/wechat", h.WechatPayNotify)
 | 
			
		||||
			group.POST("notify/hupi", h.HuPiPayNotify)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/product/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("enable", h.Enable)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/order/")
 | 
			
		||||
			group.POST("list", h.List)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("clear", h.Clear)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/order/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("query", h.Query)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/product/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(handler.NewInviteHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/invite/")
 | 
			
		||||
			group.GET("code", h.Code)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("hits", h.Hits)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(admin.NewFunctionHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/function/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("token", h.GenToken)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(admin.NewUploadHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
 | 
			
		||||
			s.Engine.POST("/api/admin/upload", h.Upload)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(handler.NewFunctionHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/function/")
 | 
			
		||||
			group.POST("weibo", h.WeiBo)
 | 
			
		||||
			group.POST("zaobao", h.ZaoBao)
 | 
			
		||||
			group.POST("dalle3", h.Dall3)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/chat/")
 | 
			
		||||
			group.POST("list", h.List)
 | 
			
		||||
			group.POST("message", h.Messages)
 | 
			
		||||
			group.GET("history", h.History)
 | 
			
		||||
			group.GET("remove", h.RemoveChat)
 | 
			
		||||
			group.GET("message/remove", h.RemoveMessage)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/powerLog/")
 | 
			
		||||
			group.POST("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.PowerLogHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/powerLog/")
 | 
			
		||||
			group.POST("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(admin.NewMenuHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/menu/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("enable", h.Enable)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewMenuHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/menu/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewMarkMapHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
 | 
			
		||||
			s.Engine.POST("/api/markMap/gen", h.Generate)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewDallJobHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/dall")
 | 
			
		||||
			group.POST("image", h.Image)
 | 
			
		||||
			group.GET("jobs", h.JobList)
 | 
			
		||||
			group.GET("imgWall", h.ImgWall)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("publish", h.Publish)
 | 
			
		||||
			group.GET("models", h.GetModels)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewSunoHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/suno")
 | 
			
		||||
			group.POST("create", h.Create)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("publish", h.Publish)
 | 
			
		||||
			group.POST("update", h.Update)
 | 
			
		||||
			group.GET("detail", h.Detail)
 | 
			
		||||
			group.GET("play", h.Play)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewVideoHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/video")
 | 
			
		||||
			group.POST("luma/create", h.LumaCreate)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("publish", h.Publish)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(admin.NewChatAppTypeHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/app/type")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.POST("enable", h.Enable)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewChatAppTypeHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ChatAppTypeHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/app/type")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewTestHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/test")
 | 
			
		||||
			group.Any("sse", h.PostTest, h.SseTest)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(service.NewWebsocketService),
 | 
			
		||||
		fx.Provide(handler.NewWebsocketHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.WebsocketHandler) {
 | 
			
		||||
			s.Engine.Any("/api/ws", h.Client)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewPromptHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/prompt")
 | 
			
		||||
			group.POST("/lyric", h.Lyric)
 | 
			
		||||
			group.POST("/image", h.Image)
 | 
			
		||||
			group.POST("/video", h.Video)
 | 
			
		||||
			group.POST("/meta", h.MetaPrompt)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
 | 
			
		||||
			go func() {
 | 
			
		||||
				err := s.Run(db)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error(err)
 | 
			
		||||
					os.Exit(0)
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(NewAppLifeCycle),
 | 
			
		||||
		// 注册生命周期回调函数
 | 
			
		||||
		fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) {
 | 
			
		||||
			lifecycle.Append(fx.Hook{
 | 
			
		||||
@@ -245,6 +548,26 @@ func main() {
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(admin.NewImageHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/image")
 | 
			
		||||
			group.POST("/list/mj", h.MjList)
 | 
			
		||||
			group.POST("/list/sd", h.SdList)
 | 
			
		||||
			group.POST("/list/dall", h.DallList)
 | 
			
		||||
			group.GET("/remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(admin.NewMediaHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/media")
 | 
			
		||||
			group.POST("/list/suno", h.SunoList)
 | 
			
		||||
			group.POST("/list/luma", h.LumaList)
 | 
			
		||||
			group.GET("/remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(handler.NewRealtimeHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) {
 | 
			
		||||
			s.Engine.Any("/api/realtime", h.Connection)
 | 
			
		||||
			s.Engine.POST("/api/realtime/voice", h.VoiceChat)
 | 
			
		||||
		}),
 | 
			
		||||
	)
 | 
			
		||||
	// 启动应用程序
 | 
			
		||||
	go func() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								api/res/certs/alipay/alipayPublicCert.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								api/res/certs/alipay/alipayPublicCert.crt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDszCCApugAwIBAgIQICMRB0rBU2/rZJbfJGMYIzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
 | 
			
		||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
 | 
			
		||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
 | 
			
		||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDYzNTQxWhcNMjQxMTA2MDYzNTQxWjCB
 | 
			
		||||
hDELMAkGA1UEBhMCQ04xHzAdBgNVBAoMFm1ib25meTkwMTVAc2FuZGJveC5jb20xDzANBgNVBAsM
 | 
			
		||||
BkFsaXBheTFDMEEGA1UEAww65pSv5LuY5a6dKOS4reWbvSnnvZHnu5zmioDmnK/mnInpmZDlhazl
 | 
			
		||||
j7gtMjA4ODcyMTAyMDc1MDU4MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsoKcw5
 | 
			
		||||
sxaiyV7mpWzDtnQ1K518eQLP0+dJlZAf06aBep/Aj9DIqrba/k7DHt8dKQvILMLAMpN1+2IRxbaO
 | 
			
		||||
yxMa/laj3lZ1eHrB6F077O3D62oHcE3noZtXL0N1zZAxpmkNmYIHeLZS2oLMS4ANu47O/wpDC7BV
 | 
			
		||||
HjdpZugtdPJ4mxdCpM9GDdLs7W4s5QI4PUPK4skFNMFoKI+0cYP/9ju87UP//IHC/K510GWNl+Gn
 | 
			
		||||
Cvgag3AmiIB0utJNsGhxm6zT1T9tUWjW9iz/BxBKiPatsCX9VpPQzGnW7ZonRQtiZSokIlP2IPvl
 | 
			
		||||
H5DcwpWUz3/LUY0SmKxnKOEYeOOqCW8CAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
 | 
			
		||||
DQEBCwUAA4IBAQAtgxF2EzjOndEFxBUD9tFwcSt6XKGggOp52oft1pvynPg4ALTLafOtfEPDrFBH
 | 
			
		||||
PwpYrSu9s9C8NJtaA2HrlCfBjIuwEFTXiN+HPvS0SwSPKt9AXEiTcOF8vDcGamEen8QI4fo5Jia7
 | 
			
		||||
2VRKkerkww5/+FzSaVO7ZUKuL80M1QJStmAZc8kPPwdYOTTW2bGf8BcmSDL6SPElBkt7tCCRd4sn
 | 
			
		||||
+jq4cZ0yb2i77rBZCwHcTvfTqIBblPwLv4uGvg3+83BxIB5w6Kqp06bKEAPmobFY5IVHa+ON0/qi
 | 
			
		||||
BXxXr+WQ3piKRVQEN64+PTAjSc67Ix1umvpLl3Ko6Ry7NJmpDcUn
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDszCCApugAwIBAgIQIBkIGbgVxq210KxLJ+YA/TANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UE
 | 
			
		||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxJTAjBgNVBAsMHENlcnRpZmljYXRpb24gQXV0
 | 
			
		||||
aG9yaXR5IHRlc3QxNjA0BgNVBAMMLUFudCBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
 | 
			
		||||
dHkgUjEgdGVzdDAeFw0xOTA4MTkxMTE2MDBaFw0yNDA4MDExMTE2MDBaMIGRMQswCQYDVQQGEwJD
 | 
			
		||||
TjEbMBkGA1UECgwSQW50IEZpbmFuY2lhbCB0ZXN0MSUwIwYDVQQLDBxDZXJ0aWZpY2F0aW9uIEF1
 | 
			
		||||
dGhvcml0eSB0ZXN0MT4wPAYDVQQDDDVBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
 | 
			
		||||
aXR5IENsYXNzIDIgUjEgdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMh4FKYO
 | 
			
		||||
ZyRQHD6eFbPKZeSAnrfjfU7xmS9Yoozuu+iuqZlb6Z0SPLUqqTZAFZejOcmr07ln/pwZxluqplxC
 | 
			
		||||
5+B48End4nclDMlT5HPrDr3W0frs6Xsa2ZNcyil/iKNB5MbGll8LRAxntsKvZZj6vUTMb705gYgm
 | 
			
		||||
VUMILwi/ZxKTQqBtkT/kQQ5y6nOZsj7XI5rYdz6qqOROrpvS/d7iypdHOMIM9Iz9DlL1mrCykbBi
 | 
			
		||||
t25y+gTeXmuisHUwqaRpwtCGK4BayCqxRGbNipe6W73EK9lBrrzNtTr9NaysesT/v+l25JHCL9tG
 | 
			
		||||
wpNr1oWFzk4IHVOg0ORiQ6SUgxZUTYcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
 | 
			
		||||
DQEBCwUAA4IBAQBWThEoIaQoBX2YeRY/I8gu6TYnFXtyuCljANnXnM38ft+ikhE5mMNgKmJYLHvT
 | 
			
		||||
yWWWgwHoSAWEuml7EGbE/2AK2h3k0MdfiWLzdmpPCRG/RJHk6UB1pMHPilI+c0MVu16OPpKbg5Vf
 | 
			
		||||
LTv7dsAB40AzKsvyYw88/Ezi1osTXo6QQwda7uefvudirtb8FcQM9R66cJxl3kt1FXbpYwheIm/p
 | 
			
		||||
j1mq64swCoIYu4NrsUYtn6CV542DTQMI5QdXkn+PzUUly8F6kDp+KpMNd0avfWNL5+O++z+F5Szy
 | 
			
		||||
1CPta1D7EQ/eYmMP+mOQ35oifWIoFCpN6qQVBS/Hob1J/UUyg7BW
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
							
								
								
									
										88
									
								
								api/res/certs/alipay/alipayRootCert.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								api/res/certs/alipay/alipayRootCert.crt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG
 | 
			
		||||
EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw
 | 
			
		||||
MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO
 | 
			
		||||
UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE
 | 
			
		||||
MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT
 | 
			
		||||
V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti
 | 
			
		||||
W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ
 | 
			
		||||
MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b
 | 
			
		||||
53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI
 | 
			
		||||
pDoiVhsLwg==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIF0zCCA7ugAwIBAgIIH8+hjWpIDREwDQYJKoZIhvcNAQELBQAwejELMAkGA1UE
 | 
			
		||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmlj
 | 
			
		||||
YXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmlj
 | 
			
		||||
YXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMyMTEzNDg0MFoXDTM4MDIyODEzNDg0
 | 
			
		||||
MFowejELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNV
 | 
			
		||||
BAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5j
 | 
			
		||||
aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMIICIjANBgkqhkiG9w0BAQEF
 | 
			
		||||
AAOCAg8AMIICCgKCAgEAtytTRcBNuur5h8xuxnlKJetT65cHGemGi8oD+beHFPTk
 | 
			
		||||
rUTlFt9Xn7fAVGo6QSsPb9uGLpUFGEdGmbsQ2q9cV4P89qkH04VzIPwT7AywJdt2
 | 
			
		||||
xAvMs+MgHFJzOYfL1QkdOOVO7NwKxH8IvlQgFabWomWk2Ei9WfUyxFjVO1LVh0Bp
 | 
			
		||||
dRBeWLMkdudx0tl3+21t1apnReFNQ5nfX29xeSxIhesaMHDZFViO/DXDNW2BcTs6
 | 
			
		||||
vSWKyJ4YIIIzStumD8K1xMsoaZBMDxg4itjWFaKRgNuPiIn4kjDY3kC66Sl/6yTl
 | 
			
		||||
YUz8AybbEsICZzssdZh7jcNb1VRfk79lgAprm/Ktl+mgrU1gaMGP1OE25JCbqli1
 | 
			
		||||
Pbw/BpPynyP9+XulE+2mxFwTYhKAwpDIDKuYsFUXuo8t261pCovI1CXFzAQM2w7H
 | 
			
		||||
DtA2nOXSW6q0jGDJ5+WauH+K8ZSvA6x4sFo4u0KNCx0ROTBpLif6GTngqo3sj+98
 | 
			
		||||
SZiMNLFMQoQkjkdN5Q5g9N6CFZPVZ6QpO0JcIc7S1le/g9z5iBKnifrKxy0TQjtG
 | 
			
		||||
PsDwc8ubPnRm/F82RReCoyNyx63indpgFfhN7+KxUIQ9cOwwTvemmor0A+ZQamRe
 | 
			
		||||
9LMuiEfEaWUDK+6O0Gl8lO571uI5onYdN1VIgOmwFbe+D8TcuzVjIZ/zvHrAGUcC
 | 
			
		||||
AwEAAaNdMFswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFF90
 | 
			
		||||
tATATwda6uWx2yKjh0GynOEBMB8GA1UdIwQYMBaAFF90tATATwda6uWx2yKjh0Gy
 | 
			
		||||
nOEBMA0GCSqGSIb3DQEBCwUAA4ICAQCVYaOtqOLIpsrEikE5lb+UARNSFJg6tpkf
 | 
			
		||||
tJ2U8QF/DejemEHx5IClQu6ajxjtu0Aie4/3UnIXop8nH/Q57l+Wyt9T7N2WPiNq
 | 
			
		||||
JSlYKYbJpPF8LXbuKYG3BTFTdOVFIeRe2NUyYh/xs6bXGr4WKTXb3qBmzR02FSy3
 | 
			
		||||
IODQw5Q6zpXj8prYqFHYsOvGCEc1CwJaSaYwRhTkFedJUxiyhyB5GQwoFfExCVHW
 | 
			
		||||
05ZFCAVYFldCJvUzfzrWubN6wX0DD2dwultgmldOn/W/n8at52mpPNvIdbZb2F41
 | 
			
		||||
T0YZeoWnCJrYXjq/32oc1cmifIHqySnyMnavi75DxPCdZsCOpSAT4j4lAQRGsfgI
 | 
			
		||||
kkLPGQieMfNNkMCKh7qjwdXAVtdqhf0RVtFILH3OyEodlk1HYXqX5iE5wlaKzDop
 | 
			
		||||
PKwf2Q3BErq1xChYGGVS+dEvyXc/2nIBlt7uLWKp4XFjqekKbaGaLJdjYP5b2s7N
 | 
			
		||||
1dM0MXQ/f8XoXKBkJNzEiM3hfsU6DOREgMc1DIsFKxfuMwX3EkVQM1If8ghb6x5Y
 | 
			
		||||
jXayv+NLbidOSzk4vl5QwngO/JYFMkoc6i9LNwEaEtR9PhnrdubxmrtM+RjfBm02
 | 
			
		||||
77q3dSWFESFQ4QxYWew4pHE0DpWbWy/iMIKQ6UZ5RLvB8GEcgt8ON7BBJeMc+Dyi
 | 
			
		||||
kT9qhqn+lw==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIICiDCCAgygAwIBAgIIQX76UsB/30owDAYIKoZIzj0EAwMFADB6MQswCQYDVQQG
 | 
			
		||||
EwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UECwwXQ2VydGlmaWNh
 | 
			
		||||
dGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNpYWwgQ2VydGlmaWNh
 | 
			
		||||
dGlvbiBBdXRob3JpdHkgRTEwHhcNMTkwNDI4MTYyMDQ0WhcNNDkwNDIwMTYyMDQ0
 | 
			
		||||
WjB6MQswCQYDVQQGEwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UE
 | 
			
		||||
CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNp
 | 
			
		||||
YWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRTEwdjAQBgcqhkjOPQIBBgUrgQQA
 | 
			
		||||
IgNiAASCCRa94QI0vR5Up9Yr9HEupz6hSoyjySYqo7v837KnmjveUIUNiuC9pWAU
 | 
			
		||||
WP3jwLX3HkzeiNdeg22a0IZPoSUCpasufiLAnfXh6NInLiWBrjLJXDSGaY7vaokt
 | 
			
		||||
rpZvAdmjXTBbMAsGA1UdDwQEAwIBBjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRZ
 | 
			
		||||
4ZTgDpksHL2qcpkFkxD2zVd16TAfBgNVHSMEGDAWgBRZ4ZTgDpksHL2qcpkFkxD2
 | 
			
		||||
zVd16TAMBggqhkjOPQQDAwUAA2gAMGUCMQD4IoqT2hTUn0jt7oXLdMJ8q4vLp6sg
 | 
			
		||||
wHfPiOr9gxreb+e6Oidwd2LDnC4OUqCWiF8CMAzwKs4SnDJYcMLf2vpkbuVE4dTH
 | 
			
		||||
Rglz+HGcTLWsFs4KxLsq7MuU+vJTBUeDJeDjdA==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDxTCCAq2gAwIBAgIUEMdk6dVgOEIS2cCP0Q43P90Ps5YwDQYJKoZIhvcNAQEF
 | 
			
		||||
BQAwajELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM
 | 
			
		||||
E0NoaW5hIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMMH2lUcnVzQ2hpbmEgQ2xhc3Mg
 | 
			
		||||
MiBSb290IENBIC0gRzMwHhcNMTMwNDE4MDkzNjU2WhcNMzMwNDE4MDkzNjU2WjBq
 | 
			
		||||
MQswCQYDVQQGEwJDTjETMBEGA1UECgwKaVRydXNDaGluYTEcMBoGA1UECwwTQ2hp
 | 
			
		||||
bmEgVHJ1c3QgTmV0d29yazEoMCYGA1UEAwwfaVRydXNDaGluYSBDbGFzcyAyIFJv
 | 
			
		||||
b3QgQ0EgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPPShpV
 | 
			
		||||
nJbMqqCw6Bz1kehnoPst9pkr0V9idOwU2oyS47/HjJXk9Rd5a9xfwkPO88trUpz5
 | 
			
		||||
4GmmwspDXjVFu9L0eFaRuH3KMha1Ak01citbF7cQLJlS7XI+tpkTGHEY5pt3EsQg
 | 
			
		||||
wykfZl/A1jrnSkspMS997r2Gim54cwz+mTMgDRhZsKK/lbOeBPpWtcFizjXYCqhw
 | 
			
		||||
WktvQfZBYi6o4sHCshnOswi4yV1p+LuFcQ2ciYdWvULh1eZhLxHbGXyznYHi0dGN
 | 
			
		||||
z+I9H8aXxqAQfHVhbdHNzi77hCxFjOy+hHrGsyzjrd2swVQ2iUWP8BfEQqGLqM1g
 | 
			
		||||
KgWKYfcTGdbPB1MCAwEAAaNjMGEwHQYDVR0OBBYEFG/oAMxTVe7y0+408CTAK8hA
 | 
			
		||||
uTyRMB8GA1UdIwQYMBaAFG/oAMxTVe7y0+408CTAK8hAuTyRMA8GA1UdEwEB/wQF
 | 
			
		||||
MAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBLnUTfW7hp
 | 
			
		||||
emMbuUGCk7RBswzOT83bDM6824EkUnf+X0iKS95SUNGeeSWK2o/3ALJo5hi7GZr3
 | 
			
		||||
U8eLaWAcYizfO99UXMRBPw5PRR+gXGEronGUugLpxsjuynoLQu8GQAeysSXKbN1I
 | 
			
		||||
UugDo9u8igJORYA+5ms0s5sCUySqbQ2R5z/GoceyI9LdxIVa1RjVX8pYOj8JFwtn
 | 
			
		||||
DJN3ftSFvNMYwRuILKuqUYSHc2GPYiHVflDh5nDymCMOQFcFG3WsEuB+EYQPFgIU
 | 
			
		||||
1DHmdZcz7Llx8UOZXX2JupWCYzK1XhJb+r4hK5ncf/w8qGtYlmyJpxk3hr1TfUJX
 | 
			
		||||
Yf4Zr0fJsGuv
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
							
								
								
									
										19
									
								
								api/res/certs/alipay/appPublicCert.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								api/res/certs/alipay/appPublicCert.crt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDmTCCAoGgAwIBAgIQICMRB2LW76yahgdg3IFNPDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
 | 
			
		||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
 | 
			
		||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
 | 
			
		||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDU0NjE5WhcNMjQxMTExMDU0NjE5WjBr
 | 
			
		||||
MQswCQYDVQQGEwJDTjEfMB0GA1UECgwWbWJvbmZ5OTAxNUBzYW5kYm94LmNvbTEPMA0GA1UECwwG
 | 
			
		||||
QWxpcGF5MSowKAYDVQQDDCEyMDg4NzIxMDIwNzUwNTgxLTkwMjEwMDAxMzE2NTgwMjMwggEiMA0G
 | 
			
		||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxihQPf1Q+g9ArgM46shVqL5sbRha/df95D1PsWyEq
 | 
			
		||||
ANmWmG4zZ+ksYDVQrc4KzhSRoi56sm/7TDFYTmM6bW99e/nKW58WxyZB4ie5qA3F4n17psPyDqb8
 | 
			
		||||
IokcQmCphSFDaXQD6AoXoLNtTM0vAI2cWxAgebZ/vsrdj5Ntjt+Rp3NYMCk1i5xovHcfILzLEGbX
 | 
			
		||||
QXoT9fo5AhHotTWa6xHVLPUGY9qwLzQxHzBmvy5ZMfnOfJkm/mDisTSqAUB59F3dzU/1ARVkEZ1w
 | 
			
		||||
Mgb4XohWBw6iurQfbMnH2mIomAAwwZVFv+sXDbL9yMbSMo/SjVsTQprn0Q0EnwLo7nmmOM6HAgMB
 | 
			
		||||
AAGjEjAQMA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEAn3Y4/C1h9R6ONsBqX3/q
 | 
			
		||||
XfHX7yX1FM0Y1x48X3/Yxk6HivAkTukhhhVYVKJsbrbzRqHDp9vhAP/FR6o6pAevaYMmLov0VMXU
 | 
			
		||||
7oAuetgkaYEYkDuNen5/Hpdhqi2vTtdT+q9w8zHJd6MDQ0aoHgIxpLKw5vof2R1N4fwSgNXMiXE5
 | 
			
		||||
kmllKQMem/+on2p+Sj80/2asxryHIGlH87qPzkffv+kIOkZthbTApTFLLjdVri2QHGe8/cc4xy01
 | 
			
		||||
/9iR3IUzNahotT41lJ4bMevBY7XMAS3n5ekyABN/9ZRJqhWdXgmFCRN/u56qd6lDgu7R2M2QUoyc
 | 
			
		||||
LuW5DfgRItKlmUB7sw==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
							
								
								
									
										1
									
								
								api/res/certs/alipay/privateKey.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								api/res/certs/alipay/privateKey.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
MIIEpQIBAAKCAQEAsYoUD39UPoPQK4DOOrIVai+bG0YWv3X/eQ9T7FshKgDZlphuM2fpLGA1UK3OCs4UkaIuerJv+0wxWE5jOm1vfXv5ylufFscmQeInuagNxeJ9e6bD8g6m/CKJHEJgqYUhQ2l0A+gKF6CzbUzNLwCNnFsQIHm2f77K3Y+TbY7fkadzWDApNYucaLx3HyC8yxBm10F6E/X6OQIR6LU1musR1Sz1BmPasC80MR8wZr8uWTH5znyZJv5g4rE0qgFAefRd3c1P9QEVZBGdcDIG+F6IVgcOorq0H2zJx9piKJgAMMGVRb/rFw2y/cjG0jKP0o1bE0Ka59ENBJ8C6O55pjjOhwIDAQABAoIBAFetNfz1R7hbxjlFshMAkVzQR8wvT9qbvl+dtzdZRcaFhu89NecDIP7+QDYor0FcxoGpU0TazDyRQyk2BQD8vHt+9zv9BVLtZLJSqoWgPbUFBi1DjS8EF2ka8RVYnn35NhUhhd7L//ftL88Bh673mfembQ9srDjoEy1Z01feoABAnCMkNFl986DmEwnarvEufXSDIgeN4ioMxha4NvfIPuI0zpVdV1O9sv+SGC+VEWZBtN3GNsaf4zS/f8FVGvTiU/Abz0gSw/iwSPHclDWQDTN3yFHf/tfqlzh0mH0WfhnuOBFWXzK+R7fbnM+asI9ttvzRcfpzgRGXdPcNcOv/6cECgYEA3DVqpi1k8MYfJixju6SG5gfyhM4VFksFmCMaNPgtatDMBKLMTgV/Ej6LXREojcy29uZl83F09pVlpd41eG39ULIPktixA/BqErQ2UaWh6kOxifycpu22Jh0r09hax6UgVrcBrrnCJEjcFsuJlrZvXQSzc3PBxjWy5gjabS5h9iECgYEAzmVAIh2frF01Y95zsLueAhhZwCtPanm6kf7ivR4r1plIX3b2sNRhWGmEHFgaCE6Braa0ogQ73Hd26kw4ZW+D6QMGC/zjCBEzDLLf++SjdVUHiY5AR4WHqXzq1jdAlsVyo9R661oAOp3lhiJVGLNXkHyEfEVPHsaxJh4osYSbX6cCgYEAx32Qx0i6eDFTyLZQB46uMrgiaVN04QRH5iJuvGvUYT8UhGKjaU8rZfDJOh+wOH2rhxMEaz1uc3C2bERY9mfWI4Ob/jFWc7YZsiYWS3Mcsuhubw4tMECLUg39RWZsHw8ls8kIuixIh6yFzhTH6YQOcRswIrhMZG8DScfdcSmiz2ECgYEAkWP1t5KSpkLKl11etcKUXfl1T8+yk9jIOowIgRw92WAFAWq2AH67TCKYM7dEL1HOO9tRJ0hAOt/U3ttuZtYVYBEHM26jJ02mXm2rJrA7DS4mrxmL4lYH6LbcXqZxU0Qnq4zEQgIWYzRTORf6Rfof1uJAGaJhR9bDd4yLMfGt2cUCgYEAo216Y61xOHUTA4AF1eekk+r+uOcQgQDvLXfs9FkDdJLk0mPG48/+eIYpPFnANJ/riF/DWOp8WGEe2IzA9yUFexzDbNQK8ha9kGcxaSAyiCwzjZ/t9/+hScDSV8kNqWSRSisu/YOFleEHbokT6mbLZ+gdqES8mUUanaEBzRQYGxo=
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								api/res/img/alipay.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								api/res/img/alipay.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								api/res/img/geek-pay.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								api/res/img/geek-pay.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 27 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								api/res/img/qq-pay.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								api/res/img/qq-pay.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 17 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								api/res/img/wechat-pay.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								api/res/img/wechat-pay.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.7 KiB  | 
@@ -1,19 +1,26 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type CaptchaService struct {
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	config types.ApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService {
 | 
			
		||||
func NewCaptchaService(config types.ApiConfig) *CaptchaService {
 | 
			
		||||
	return &CaptchaService{
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second),
 | 
			
		||||
@@ -60,3 +67,44 @@ func (s *CaptchaService) Check(data interface{}) bool {
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *CaptchaService) SlideGet() (interface{}, error) {
 | 
			
		||||
	if s.config.Token == "" {
 | 
			
		||||
		return nil, errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/captcha/slide/get", s.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := s.client.R().
 | 
			
		||||
		SetHeader("AppId", s.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return nil, fmt.Errorf("请求 API 失败:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return nil, fmt.Errorf("请求 API 失败:%s", res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.Data, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *CaptchaService) SlideCheck(data interface{}) bool {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/captcha/slide/check", s.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := s.client.R().
 | 
			
		||||
		SetHeader("AppId", s.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
 | 
			
		||||
		SetBodyJsonMarshal(data).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										333
									
								
								api/service/dalle/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										333
									
								
								api/service/dalle/service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,333 @@
 | 
			
		||||
package dalle
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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/types"
 | 
			
		||||
	logger2 "geekai/logger"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/oss"
 | 
			
		||||
	"geekai/store"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"io"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
// DALL-E 绘画服务
 | 
			
		||||
 | 
			
		||||
type Service struct {
 | 
			
		||||
	httpClient    *req.Client
 | 
			
		||||
	db            *gorm.DB
 | 
			
		||||
	uploadManager *oss.UploaderManager
 | 
			
		||||
	taskQueue     *store.RedisQueue
 | 
			
		||||
	notifyQueue   *store.RedisQueue
 | 
			
		||||
	userService   *service.UserService
 | 
			
		||||
	wsService     *service.WebsocketService
 | 
			
		||||
	clientIds     map[uint]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService, wsService *service.WebsocketService) *Service {
 | 
			
		||||
	return &Service{
 | 
			
		||||
		httpClient:    req.C().SetTimeout(time.Minute * 3),
 | 
			
		||||
		db:            db,
 | 
			
		||||
		taskQueue:     store.NewRedisQueue("DallE_Task_Queue", redisCli),
 | 
			
		||||
		notifyQueue:   store.NewRedisQueue("DallE_Notify_Queue", redisCli),
 | 
			
		||||
		wsService:     wsService,
 | 
			
		||||
		uploadManager: manager,
 | 
			
		||||
		userService:   userService,
 | 
			
		||||
		clientIds:     map[uint]string{},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushTask push a new mj task in to task queue
 | 
			
		||||
func (s *Service) PushTask(task types.DallTask) {
 | 
			
		||||
	logger.Infof("add a new DALL-E task to the task list: %+v", task)
 | 
			
		||||
	s.taskQueue.RPush(task)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Run() {
 | 
			
		||||
	// 将数据库中未提交的人物加载到队列
 | 
			
		||||
	var jobs []model.DallJob
 | 
			
		||||
	s.db.Where("progress", 0).Find(&jobs)
 | 
			
		||||
	for _, v := range jobs {
 | 
			
		||||
		var task types.DallTask
 | 
			
		||||
		err := utils.JsonDecode(v.TaskInfo, &task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("decode task info with error: %v", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		task.Id = v.Id
 | 
			
		||||
		s.PushTask(task)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Info("Starting DALL-E job consumer...")
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			var task types.DallTask
 | 
			
		||||
			err := s.taskQueue.LPop(&task)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Errorf("taking task with error: %v", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			logger.Infof("handle a new DALL-E task: %+v", task)
 | 
			
		||||
			s.clientIds[task.Id] = task.ClientId
 | 
			
		||||
			_, err = s.Image(task, false)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Errorf("error with image task: %v", err)
 | 
			
		||||
				s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
 | 
			
		||||
					"progress": service.FailTaskProgress,
 | 
			
		||||
					"err_msg":  err.Error(),
 | 
			
		||||
				})
 | 
			
		||||
				s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.Id), Message: service.TaskStatusFailed})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type imgReq struct {
 | 
			
		||||
	Model   string `json:"model"`
 | 
			
		||||
	Prompt  string `json:"prompt"`
 | 
			
		||||
	N       int    `json:"n,omitempty"`
 | 
			
		||||
	Size    string `json:"size,omitempty"`
 | 
			
		||||
	Quality string `json:"quality,omitempty"`
 | 
			
		||||
	Style   string `json:"style,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type imgRes struct {
 | 
			
		||||
	Created int64 `json:"created"`
 | 
			
		||||
	Data    []struct {
 | 
			
		||||
		RevisedPrompt string `json:"revised_prompt,omitempty"`
 | 
			
		||||
		Url           string `json:"url,omitempty"`
 | 
			
		||||
		B64Json       string `json:"b64_json,omitempty"`
 | 
			
		||||
	} `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ErrRes struct {
 | 
			
		||||
	Error struct {
 | 
			
		||||
		Code    interface{} `json:"code"`
 | 
			
		||||
		Message string      `json:"message"`
 | 
			
		||||
		Param   interface{} `json:"param"`
 | 
			
		||||
		Type    string      `json:"type"`
 | 
			
		||||
	} `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
 | 
			
		||||
	logger.Debugf("绘画参数:%+v", task)
 | 
			
		||||
	prompt := task.Prompt
 | 
			
		||||
	// translate prompt
 | 
			
		||||
	if utils.HasChinese(prompt) {
 | 
			
		||||
		content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, prompt), task.TranslateModelId)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			prompt = content
 | 
			
		||||
			logger.Debugf("重写后提示词:%s", prompt)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chatModel model.ChatModel
 | 
			
		||||
	s.db.Where("id = ?", task.ModelId).First(&chatModel)
 | 
			
		||||
 | 
			
		||||
	// get image generation API KEY
 | 
			
		||||
	var apiKey model.ApiKey
 | 
			
		||||
	session := s.db.Where("enabled", true)
 | 
			
		||||
	if chatModel.KeyId > 0 {
 | 
			
		||||
		session = session.Where("id = ?", chatModel.KeyId)
 | 
			
		||||
	} else {
 | 
			
		||||
		session = session.Where("type = ?", "dalle")
 | 
			
		||||
	}
 | 
			
		||||
	err := session.Order("last_used_at ASC").First(&apiKey).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("no available Image Generation api key: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var res imgRes
 | 
			
		||||
	var errRes ErrRes
 | 
			
		||||
	if len(apiKey.ProxyURL) > 5 {
 | 
			
		||||
		s.httpClient.SetProxyURL(apiKey.ProxyURL).R()
 | 
			
		||||
	}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
 | 
			
		||||
	reqBody := imgReq{
 | 
			
		||||
		Model:   chatModel.Value,
 | 
			
		||||
		Prompt:  prompt,
 | 
			
		||||
		N:       1,
 | 
			
		||||
		Size:    task.Size,
 | 
			
		||||
		Style:   task.Style,
 | 
			
		||||
		Quality: task.Quality,
 | 
			
		||||
	}
 | 
			
		||||
	logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
 | 
			
		||||
	r, err := s.httpClient.R().SetHeader("Body-Type", "application/json").
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+apiKey.Value).
 | 
			
		||||
		SetBody(reqBody).
 | 
			
		||||
		SetErrorResult(&errRes).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorf("error with send request: %v", err)
 | 
			
		||||
		return "", fmt.Errorf("error with send request: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		logger.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error)
 | 
			
		||||
		return "", fmt.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	all, _ := io.ReadAll(r.Body)
 | 
			
		||||
	logger.Debugf("response: %+v", string(all))
 | 
			
		||||
 | 
			
		||||
	// update the api key last use time
 | 
			
		||||
	s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
	var imgURL string
 | 
			
		||||
	var data = map[string]interface{}{
 | 
			
		||||
		"progress": 100,
 | 
			
		||||
		"prompt":   prompt,
 | 
			
		||||
	}
 | 
			
		||||
	// 如果返回的是base64,则需要上传到oss
 | 
			
		||||
	if res.Data[0].B64Json != "" {
 | 
			
		||||
		imgURL, err = s.uploadManager.GetUploadHandler().PutBase64(res.Data[0].B64Json)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("error with upload image: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		logger.Infof("upload image to oss: %s", imgURL)
 | 
			
		||||
		data["img_url"] = imgURL
 | 
			
		||||
	} else {
 | 
			
		||||
		imgURL = res.Data[0].Url
 | 
			
		||||
	}
 | 
			
		||||
	data["org_url"] = imgURL
 | 
			
		||||
	// update task progress
 | 
			
		||||
	err = s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(data).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("err with update database: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.Id), Message: service.TaskStatusFailed})
 | 
			
		||||
	var content string
 | 
			
		||||
	if sync {
 | 
			
		||||
		imgURL, err := s.downloadImage(task.Id, int(task.UserId), res.Data[0].Url)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("error with download image: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n\n", prompt, imgURL)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return content, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) CheckTaskNotify() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		logger.Info("Running DALL-E task notify checking ...")
 | 
			
		||||
		for {
 | 
			
		||||
			var message service.NotifyMessage
 | 
			
		||||
			err := s.notifyQueue.LPop(&message)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			logger.Debugf("notify message: %+v", message)
 | 
			
		||||
			client := s.wsService.Clients.Get(message.ClientId)
 | 
			
		||||
			if client == nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			utils.SendChannelMsg(client, types.ChDall, message.Message)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) CheckTaskStatus() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		logger.Info("Running DALL-E task status checking ...")
 | 
			
		||||
		for {
 | 
			
		||||
			// 检查未完成任务进度
 | 
			
		||||
			var jobs []model.DallJob
 | 
			
		||||
			s.db.Where("progress < ?", 100).Find(&jobs)
 | 
			
		||||
			for _, job := range jobs {
 | 
			
		||||
				// 超时的任务标记为失败
 | 
			
		||||
				if time.Now().Sub(job.CreatedAt) > time.Minute*10 {
 | 
			
		||||
					job.Progress = service.FailTaskProgress
 | 
			
		||||
					job.ErrMsg = "任务超时"
 | 
			
		||||
					s.db.Updates(&job)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 找出失败的任务,并恢复其扣减算力
 | 
			
		||||
			s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs)
 | 
			
		||||
			for _, job := range jobs {
 | 
			
		||||
				var task types.DallTask
 | 
			
		||||
				err := utils.JsonDecode(job.TaskInfo, &task)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				err = s.userService.IncreasePower(int(job.UserId), job.Power, model.PowerLog{
 | 
			
		||||
					Type:   types.PowerRefund,
 | 
			
		||||
					Model:  task.ModelName,
 | 
			
		||||
					Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg),
 | 
			
		||||
				})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				// 更新任务状态
 | 
			
		||||
				s.db.Model(&job).UpdateColumn("power", 0)
 | 
			
		||||
			}
 | 
			
		||||
			time.Sleep(time.Second * 10)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) DownloadImages() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		var items []model.DallJob
 | 
			
		||||
		for {
 | 
			
		||||
			res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
 | 
			
		||||
			if res.Error != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// download images
 | 
			
		||||
			for _, v := range items {
 | 
			
		||||
				if v.OrgURL == "" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				logger.Infof("try to download image: %s", v.OrgURL)
 | 
			
		||||
				imgURL, err := s.downloadImage(v.Id, int(v.UserId), v.OrgURL)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error("error with download image: %s, error: %v", imgURL, err)
 | 
			
		||||
					continue
 | 
			
		||||
				} else {
 | 
			
		||||
					logger.Infof("download image %s successfully.", v.OrgURL)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			time.Sleep(time.Second * 5)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
 | 
			
		||||
	// sava image
 | 
			
		||||
	imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// update img_url
 | 
			
		||||
	res := s.db.Model(&model.DallJob{Id: jobId}).UpdateColumn("img_url", imgURL)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[jobId], UserId: userId, JobId: int(jobId), Message: service.TaskStatusFinished})
 | 
			
		||||
	return imgURL, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Function interface {
 | 
			
		||||
	Invoke(map[string]interface{}) (string, error)
 | 
			
		||||
	Name() string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFunctions(config *types.AppConfig) map[string]Function {
 | 
			
		||||
	return map[string]Function{
 | 
			
		||||
		types.FuncZaoBao:     NewZaoBao(config.ApiConfig),
 | 
			
		||||
		types.FuncWeibo:      NewWeiboHot(config.ApiConfig),
 | 
			
		||||
		types.FuncHeadLine:   NewHeadLines(config.ApiConfig),
 | 
			
		||||
		types.FuncMidJourney: NewMidJourneyFunc(config.ExtConfig),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,117 +0,0 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AI 绘画函数
 | 
			
		||||
 | 
			
		||||
type FuncMidJourney struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusExtConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMidJourneyFunc(config types.ChatPlusExtConfig) FuncMidJourney {
 | 
			
		||||
	return FuncMidJourney{
 | 
			
		||||
		name:   "MidJourney AI 绘画",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(30 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Infof("MJ 绘画参数:%+v", params)
 | 
			
		||||
	prompt := utils.InterfaceToString(params["prompt"])
 | 
			
		||||
	if !utils.IsEmptyValue(params["ar"]) {
 | 
			
		||||
		prompt = fmt.Sprintf("%s --ar %s", prompt, params["ar"])
 | 
			
		||||
		delete(params, "--ar")
 | 
			
		||||
	}
 | 
			
		||||
	if !utils.IsEmptyValue(params["niji"]) {
 | 
			
		||||
		prompt = fmt.Sprintf("%s --niji %s", prompt, params["niji"])
 | 
			
		||||
		delete(params, "niji")
 | 
			
		||||
	} else {
 | 
			
		||||
		prompt = prompt + " --v 5.2"
 | 
			
		||||
	}
 | 
			
		||||
	params["prompt"] = prompt
 | 
			
		||||
	url := fmt.Sprintf("%s/api/mj/image", f.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("Authorization", f.config.Token).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(params).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", r.String(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return prompt, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MjUpscaleReq struct {
 | 
			
		||||
	Index       int32  `json:"index"`
 | 
			
		||||
	MessageId   string `json:"message_id"`
 | 
			
		||||
	MessageHash string `json:"message_hash"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncMidJourney) Upscale(upReq MjUpscaleReq) error {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/mj/upscale", f.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("Authorization", f.config.Token).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(upReq).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return fmt.Errorf("%v%v", r.String(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MjVariationReq struct {
 | 
			
		||||
	Index       int32  `json:"index"`
 | 
			
		||||
	MessageId   string `json:"message_id"`
 | 
			
		||||
	MessageHash string `json:"message_hash"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncMidJourney) Variation(upReq MjVariationReq) error {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/mj/variation", f.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("Authorization", f.config.Token).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(upReq).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return fmt.Errorf("%v%v", r.String(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncMidJourney) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncMidJourney{}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 今日头条函数实现
 | 
			
		||||
 | 
			
		||||
type FuncHeadlines struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines {
 | 
			
		||||
	return FuncHeadlines{
 | 
			
		||||
		name:   "今日头条",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncHeadlines) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/headline/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncHeadlines) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncHeadlines{}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 微博热搜函数实现
 | 
			
		||||
 | 
			
		||||
type FuncWeiboHot struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot {
 | 
			
		||||
	return FuncWeiboHot{
 | 
			
		||||
		name:   "微博热搜",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncWeiboHot) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/weibo/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncWeiboHot) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncWeiboHot{}
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 每日早报函数实现
 | 
			
		||||
 | 
			
		||||
type FuncZaoBao struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao {
 | 
			
		||||
	return FuncZaoBao{
 | 
			
		||||
		name:   "每日早报",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncZaoBao) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/zaobao/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncZaoBao) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncZaoBao{}
 | 
			
		||||
							
								
								
									
										197
									
								
								api/service/license_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								api/service/license_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,197 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
 | 
			
		||||
// * Use of this source code is governed by a Apache-2.0 license
 | 
			
		||||
// * that can be found in the LICENSE file.
 | 
			
		||||
// * @Author yangjian102621@163.com
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/store"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LicenseService struct {
 | 
			
		||||
	config       types.ApiConfig
 | 
			
		||||
	levelDB      *store.LevelDB
 | 
			
		||||
	license      *types.License
 | 
			
		||||
	urlWhiteList []string
 | 
			
		||||
	machineId    string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLicenseService(server *core.AppServer, levelDB *store.LevelDB) *LicenseService {
 | 
			
		||||
	var license types.License
 | 
			
		||||
	return &LicenseService{
 | 
			
		||||
		config:    server.Config.ApiConfig,
 | 
			
		||||
		levelDB:   levelDB,
 | 
			
		||||
		license:   &license,
 | 
			
		||||
		machineId: "",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type License struct {
 | 
			
		||||
	Name      string              `json:"name"`
 | 
			
		||||
	License   string              `json:"license"`
 | 
			
		||||
	MachineId string              `json:"mid"`
 | 
			
		||||
	ActiveAt  int64               `json:"active_at"`
 | 
			
		||||
	ExpiredAt int64               `json:"expired_at"`
 | 
			
		||||
	UserNum   int                 `json:"user_num"`
 | 
			
		||||
	Configs   types.LicenseConfig `json:"configs"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ActiveLicense 激活 License
 | 
			
		||||
func (s *LicenseService) ActiveLicense(license string, machineId string) error {
 | 
			
		||||
	var res struct {
 | 
			
		||||
		Code    types.BizCode `json:"code"`
 | 
			
		||||
		Message string        `json:"message"`
 | 
			
		||||
		Data    License       `json:"data"`
 | 
			
		||||
	}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/active")
 | 
			
		||||
	response, err := req.C().R().
 | 
			
		||||
		SetBody(map[string]string{"license": license, "machine_id": machineId}).
 | 
			
		||||
		SetSuccessResult(&res).Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("发送激活请求失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if response.IsErrorState() {
 | 
			
		||||
		return fmt.Errorf("发送激活请求失败:%v", response.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return fmt.Errorf("激活失败:%v", res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.license = &types.License{
 | 
			
		||||
		Key:       license,
 | 
			
		||||
		MachineId: machineId,
 | 
			
		||||
		Configs:   res.Data.Configs,
 | 
			
		||||
		ExpiredAt: res.Data.ExpiredAt,
 | 
			
		||||
		IsActive:  true,
 | 
			
		||||
	}
 | 
			
		||||
	err = s.levelDB.Put(types.LicenseKey, s.license)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("保存许可证书失败:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SyncLicense 定期同步 License
 | 
			
		||||
func (s *LicenseService) SyncLicense() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		retryCounter := 0
 | 
			
		||||
		for {
 | 
			
		||||
			license, err := s.fetchLicense()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				retryCounter++
 | 
			
		||||
				if retryCounter < 5 {
 | 
			
		||||
					logger.Warn(err)
 | 
			
		||||
				}
 | 
			
		||||
				s.license.IsActive = false
 | 
			
		||||
			} else {
 | 
			
		||||
				s.license = license
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			urls, err := s.fetchUrlWhiteList()
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				s.urlWhiteList = urls
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			time.Sleep(time.Second * 10)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *LicenseService) fetchLicense() (*types.License, error) {
 | 
			
		||||
	//var res struct {
 | 
			
		||||
	//	Code    types.BizCode `json:"code"`
 | 
			
		||||
	//	Message string        `json:"message"`
 | 
			
		||||
	//	Data    License       `json:"data"`
 | 
			
		||||
	//}
 | 
			
		||||
	//apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/check")
 | 
			
		||||
	//response, err := req.C().R().
 | 
			
		||||
	//	SetBody(map[string]string{"license": s.license.Key, "machine_id": s.machineId}).
 | 
			
		||||
	//	SetSuccessResult(&res).Post(apiURL)
 | 
			
		||||
	//if err != nil {
 | 
			
		||||
	//	return nil, fmt.Errorf("发送激活请求失败: %v", err)
 | 
			
		||||
	//}
 | 
			
		||||
	//if response.IsErrorState() {
 | 
			
		||||
	//	return nil, fmt.Errorf("激活失败:%v", response.Status)
 | 
			
		||||
	//}
 | 
			
		||||
	//if res.Code != types.Success {
 | 
			
		||||
	//	return nil, fmt.Errorf("激活失败:%v", res.Message)
 | 
			
		||||
	//}
 | 
			
		||||
 | 
			
		||||
	return &types.License{
 | 
			
		||||
		Key:       "abc",
 | 
			
		||||
		MachineId: "abc",
 | 
			
		||||
		Configs: types.LicenseConfig{
 | 
			
		||||
			UserNum: 10000,
 | 
			
		||||
			DeCopy:  false,
 | 
			
		||||
		},
 | 
			
		||||
		ExpiredAt: 0,
 | 
			
		||||
		IsActive:  true,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
 | 
			
		||||
	var res struct {
 | 
			
		||||
		Code    types.BizCode `json:"code"`
 | 
			
		||||
		Message string        `json:"message"`
 | 
			
		||||
		Data    []string      `json:"data"`
 | 
			
		||||
	}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/urls")
 | 
			
		||||
	response, err := req.C().R().SetSuccessResult(&res).Get(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("发送请求失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if response.IsErrorState() {
 | 
			
		||||
		return nil, fmt.Errorf("发送请求失败:%v", response.Status)
 | 
			
		||||
	}
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return nil, fmt.Errorf("获取白名单失败:%v", res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.Data, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetLicense 获取许可信息
 | 
			
		||||
func (s *LicenseService) GetLicense() *types.License {
 | 
			
		||||
	return s.license
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsValidApiURL 判断是否合法的中转 URL
 | 
			
		||||
func (s *LicenseService) IsValidApiURL(uri string) error {
 | 
			
		||||
	// 获得许可授权的直接放行
 | 
			
		||||
	return nil
 | 
			
		||||
	//if s.license.IsActive {
 | 
			
		||||
	//	if s.license.MachineId != s.machineId {
 | 
			
		||||
	//		return errors.New("系统使用了盗版的许可证书")
 | 
			
		||||
	//	}
 | 
			
		||||
	//
 | 
			
		||||
	//	if time.Now().Unix() > s.license.ExpiredAt {
 | 
			
		||||
	//		return errors.New("系统许可证书已经过期")
 | 
			
		||||
	//	}
 | 
			
		||||
	//	return nil
 | 
			
		||||
	//}
 | 
			
		||||
	//
 | 
			
		||||
	//if len(s.urlWhiteList) == 0 {
 | 
			
		||||
	//	urls, err := s.fetchUrlWhiteList()
 | 
			
		||||
	//	if err == nil {
 | 
			
		||||
	//		s.urlWhiteList = urls
 | 
			
		||||
	//	}
 | 
			
		||||
	//}
 | 
			
		||||
	//
 | 
			
		||||
	//for _, v := range s.urlWhiteList {
 | 
			
		||||
	//	if strings.HasPrefix(uri, v) {
 | 
			
		||||
	//		return nil
 | 
			
		||||
	//	}
 | 
			
		||||
	//}
 | 
			
		||||
	//return fmt.Errorf("当前 API 地址 %s 不在白名单列表当中。", uri)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										250
									
								
								api/service/mj/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								api/service/mj/client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,250 @@
 | 
			
		||||
package mj
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	logger2 "geekai/logger"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"io"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Client MidJourney client
 | 
			
		||||
type Client struct {
 | 
			
		||||
	client         *req.Client
 | 
			
		||||
	licenseService *service.LicenseService
 | 
			
		||||
	db             *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImageReq struct {
 | 
			
		||||
	BotType       string      `json:"botType,omitempty"`
 | 
			
		||||
	Prompt        string      `json:"prompt,omitempty"`
 | 
			
		||||
	Dimensions    string      `json:"dimensions,omitempty"`
 | 
			
		||||
	Base64Array   []string    `json:"base64Array,omitempty"`
 | 
			
		||||
	AccountFilter interface{} `json:"accountFilter,omitempty"`
 | 
			
		||||
	NotifyHook    string      `json:"notifyHook,omitempty"`
 | 
			
		||||
	State         string      `json:"state,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImageRes struct {
 | 
			
		||||
	Code        int    `json:"code"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
	Result  string `json:"result"`
 | 
			
		||||
	Channel string `json:"channel,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type QueryRes struct {
 | 
			
		||||
	Action  string `json:"action"`
 | 
			
		||||
	Buttons []struct {
 | 
			
		||||
		CustomId string `json:"customId"`
 | 
			
		||||
		Emoji    string `json:"emoji"`
 | 
			
		||||
		Label    string `json:"label"`
 | 
			
		||||
		Style    int    `json:"style"`
 | 
			
		||||
		Type     int    `json:"type"`
 | 
			
		||||
	} `json:"buttons"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
	FailReason  string `json:"failReason"`
 | 
			
		||||
	FinishTime  int    `json:"finishTime"`
 | 
			
		||||
	Id          string `json:"id"`
 | 
			
		||||
	ImageUrl    string `json:"imageUrl"`
 | 
			
		||||
	Progress    string `json:"progress"`
 | 
			
		||||
	Prompt      string `json:"prompt"`
 | 
			
		||||
	PromptEn    string `json:"promptEn"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
	StartTime  int    `json:"startTime"`
 | 
			
		||||
	State      string `json:"state"`
 | 
			
		||||
	Status     string `json:"status"`
 | 
			
		||||
	SubmitTime int    `json:"submitTime"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
func NewClient(licenseService *service.LicenseService, db *gorm.DB) *Client {
 | 
			
		||||
	return &Client{
 | 
			
		||||
		client:         req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
 | 
			
		||||
		licenseService: licenseService,
 | 
			
		||||
		db:             db,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) Imagine(task types.MjTask) (ImageRes, error) {
 | 
			
		||||
	apiPath := fmt.Sprintf("mj-%s/mj/submit/imagine", task.Mode)
 | 
			
		||||
	prompt := fmt.Sprintf("%s %s", task.Prompt, task.Params)
 | 
			
		||||
	if task.NegPrompt != "" {
 | 
			
		||||
		prompt += fmt.Sprintf(" --no %s", task.NegPrompt)
 | 
			
		||||
	}
 | 
			
		||||
	body := ImageReq{
 | 
			
		||||
		BotType:     "MID_JOURNEY",
 | 
			
		||||
		Prompt:      prompt,
 | 
			
		||||
		Base64Array: make([]string, 0),
 | 
			
		||||
	}
 | 
			
		||||
	// 生成图片 Base64 编码
 | 
			
		||||
	if len(task.ImgArr) > 0 {
 | 
			
		||||
		imageData, err := utils.DownloadImage(task.ImgArr[0], "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("error with download image: ", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	return c.doRequest(body, apiPath, task.ChannelId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Blend 融图
 | 
			
		||||
func (c *Client) Blend(task types.MjTask) (ImageRes, error) {
 | 
			
		||||
	apiPath := fmt.Sprintf("mj-%s/mj/submit/blend", task.Mode)
 | 
			
		||||
	body := ImageReq{
 | 
			
		||||
		BotType:     "MID_JOURNEY",
 | 
			
		||||
		Dimensions:  "SQUARE",
 | 
			
		||||
		Base64Array: make([]string, 0),
 | 
			
		||||
	}
 | 
			
		||||
	// 生成图片 Base64 编码
 | 
			
		||||
	if len(task.ImgArr) > 0 {
 | 
			
		||||
		for _, imgURL := range task.ImgArr {
 | 
			
		||||
			imageData, err := utils.DownloadImage(imgURL, "")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error("error with download image: ", err)
 | 
			
		||||
			} else {
 | 
			
		||||
				body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return c.doRequest(body, apiPath, task.ChannelId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SwapFace 换脸
 | 
			
		||||
func (c *Client) SwapFace(task types.MjTask) (ImageRes, error) {
 | 
			
		||||
	apiPath := fmt.Sprintf("mj-%s/mj/insight-face/swap", task.Mode)
 | 
			
		||||
	// 生成图片 Base64 编码
 | 
			
		||||
	if len(task.ImgArr) != 2 {
 | 
			
		||||
		return ImageRes{}, errors.New("参数错误,必须上传2张图片")
 | 
			
		||||
	}
 | 
			
		||||
	var sourceBase64 string
 | 
			
		||||
	var targetBase64 string
 | 
			
		||||
	imageData, err := utils.DownloadImage(task.ImgArr[0], "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("error with download image: ", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		sourceBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageData)
 | 
			
		||||
	}
 | 
			
		||||
	imageData, err = utils.DownloadImage(task.ImgArr[1], "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("error with download image: ", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		targetBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body := gin.H{
 | 
			
		||||
		"sourceBase64": sourceBase64,
 | 
			
		||||
		"targetBase64": targetBase64,
 | 
			
		||||
		"accountFilter": gin.H{
 | 
			
		||||
			"instanceId": "",
 | 
			
		||||
		},
 | 
			
		||||
		"state": "",
 | 
			
		||||
	}
 | 
			
		||||
	return c.doRequest(body, apiPath, task.ChannelId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Upscale 放大指定的图片
 | 
			
		||||
func (c *Client) Upscale(task types.MjTask) (ImageRes, error) {
 | 
			
		||||
	body := map[string]string{
 | 
			
		||||
		"customId": fmt.Sprintf("MJ::JOB::upsample::%d::%s", task.Index, task.MessageHash),
 | 
			
		||||
		"taskId":   task.MessageId,
 | 
			
		||||
	}
 | 
			
		||||
	apiPath := fmt.Sprintf("mj-%s/mj/submit/action", task.Mode)
 | 
			
		||||
	return c.doRequest(body, apiPath, task.ChannelId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Variation  以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效
 | 
			
		||||
func (c *Client) Variation(task types.MjTask) (ImageRes, error) {
 | 
			
		||||
	body := map[string]string{
 | 
			
		||||
		"customId": fmt.Sprintf("MJ::JOB::variation::%d::%s", task.Index, task.MessageHash),
 | 
			
		||||
		"taskId":   task.MessageId,
 | 
			
		||||
	}
 | 
			
		||||
	apiPath := fmt.Sprintf("mj-%s/mj/submit/action", task.Mode)
 | 
			
		||||
 | 
			
		||||
	return c.doRequest(body, apiPath, task.ChannelId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) doRequest(body interface{}, apiPath string, channel string) (ImageRes, error) {
 | 
			
		||||
	var res ImageRes
 | 
			
		||||
	session := c.db.Session(&gorm.Session{}).Where("type", "mj").Where("enabled", true)
 | 
			
		||||
	if channel != "" {
 | 
			
		||||
		session = session.Where("api_url", channel)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var apiKey model.ApiKey
 | 
			
		||||
	err := session.Order("last_used_at ASC").First(&apiKey).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = c.licenseService.IsValidApiURL(apiKey.ApiURL); err != nil {
 | 
			
		||||
		return ImageRes{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/%s", apiKey.ApiURL, apiPath)
 | 
			
		||||
	logger.Info("API URL: ", apiURL)
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+apiKey.Value).
 | 
			
		||||
		SetBody(body).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		errMsg, _ := io.ReadAll(r.Body)
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("API 返回错误:%s", string(errMsg))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// update the api key last used time
 | 
			
		||||
	if err = c.db.Model(&apiKey).Update("last_used_at", time.Now().Unix()).Error; err != nil {
 | 
			
		||||
		logger.Error("update api key last used time error: ", err)
 | 
			
		||||
	}
 | 
			
		||||
	res.Channel = apiKey.ApiURL
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) QueryTask(taskId string, channel string) (QueryRes, error) {
 | 
			
		||||
	var apiKey model.ApiKey
 | 
			
		||||
	err := c.db.Where("type", "mj").Where("enabled", true).Where("api_url", channel).First(&apiKey).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return QueryRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/mj/task/%s/fetch", apiKey.ApiURL, taskId)
 | 
			
		||||
	var res QueryRes
 | 
			
		||||
	r, err := c.client.R().SetHeader("Authorization", "Bearer "+apiKey.Value).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		Get(apiURL)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return QueryRes{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		return QueryRes{}, errors.New("error status:" + r.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										340
									
								
								api/service/mj/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								api/service/mj/service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,340 @@
 | 
			
		||||
package mj
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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/types"
 | 
			
		||||
	"geekai/service"
 | 
			
		||||
	"geekai/service/oss"
 | 
			
		||||
	"geekai/store"
 | 
			
		||||
	"geekai/store/model"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Service MJ 绘画服务
 | 
			
		||||
type Service struct {
 | 
			
		||||
	client          *Client // MJ Client
 | 
			
		||||
	taskQueue       *store.RedisQueue
 | 
			
		||||
	notifyQueue     *store.RedisQueue
 | 
			
		||||
	db              *gorm.DB
 | 
			
		||||
	wsService       *service.WebsocketService
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
	userService     *service.UserService
 | 
			
		||||
	clientIds       map[uint]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, wsService *service.WebsocketService, userService *service.UserService) *Service {
 | 
			
		||||
	return &Service{
 | 
			
		||||
		db:              db,
 | 
			
		||||
		taskQueue:       store.NewRedisQueue("MidJourney_Task_Queue", redisCli),
 | 
			
		||||
		notifyQueue:     store.NewRedisQueue("MidJourney_Notify_Queue", redisCli),
 | 
			
		||||
		client:          client,
 | 
			
		||||
		wsService:       wsService,
 | 
			
		||||
		uploaderManager: manager,
 | 
			
		||||
		clientIds:       map[uint]string{},
 | 
			
		||||
		userService:     userService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Run() {
 | 
			
		||||
	// 将数据库中未提交的人物加载到队列
 | 
			
		||||
	var jobs []model.MidJourneyJob
 | 
			
		||||
	s.db.Where("task_id", "").Where("progress", 0).Find(&jobs)
 | 
			
		||||
	for _, v := range jobs {
 | 
			
		||||
		var task types.MjTask
 | 
			
		||||
		err := utils.JsonDecode(v.TaskInfo, &task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("decode task info with error: %v", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		task.Id = v.Id
 | 
			
		||||
		s.clientIds[task.Id] = task.ClientId
 | 
			
		||||
		s.PushTask(task)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Info("Starting MidJourney job consumer for service")
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			var task types.MjTask
 | 
			
		||||
			err := s.taskQueue.LPop(&task)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Errorf("taking task with error: %v", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// translate prompt
 | 
			
		||||
			if utils.HasChinese(task.Prompt) {
 | 
			
		||||
				content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					task.Prompt = content
 | 
			
		||||
				} else {
 | 
			
		||||
					logger.Warnf("error with translate prompt: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			// translate negative prompt
 | 
			
		||||
			if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
 | 
			
		||||
				content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt), task.TranslateModelId)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					task.NegPrompt = content
 | 
			
		||||
				} else {
 | 
			
		||||
					logger.Warnf("error with translate prompt: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// use fast mode as default
 | 
			
		||||
			if task.Mode == "" {
 | 
			
		||||
				task.Mode = "fast"
 | 
			
		||||
			}
 | 
			
		||||
			s.clientIds[task.Id] = task.ClientId
 | 
			
		||||
 | 
			
		||||
			var job model.MidJourneyJob
 | 
			
		||||
			tx := s.db.Where("id = ?", task.Id).First(&job)
 | 
			
		||||
			if tx.Error != nil {
 | 
			
		||||
				logger.Error("任务不存在,任务ID:", task.TaskId)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			logger.Infof("handle a new MidJourney task: %+v", task)
 | 
			
		||||
			var res ImageRes
 | 
			
		||||
			switch task.Type {
 | 
			
		||||
			case types.TaskImage:
 | 
			
		||||
				res, err = s.client.Imagine(task)
 | 
			
		||||
				break
 | 
			
		||||
			case types.TaskUpscale:
 | 
			
		||||
				res, err = s.client.Upscale(task)
 | 
			
		||||
				break
 | 
			
		||||
			case types.TaskVariation:
 | 
			
		||||
				res, err = s.client.Variation(task)
 | 
			
		||||
				break
 | 
			
		||||
			case types.TaskBlend:
 | 
			
		||||
				res, err = s.client.Blend(task)
 | 
			
		||||
				break
 | 
			
		||||
			case types.TaskSwapFace:
 | 
			
		||||
				res, err = s.client.SwapFace(task)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != nil || (res.Code != 1 && res.Code != 22) {
 | 
			
		||||
				var errMsg string
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					errMsg = err.Error()
 | 
			
		||||
				} else {
 | 
			
		||||
					errMsg = fmt.Sprintf("%v,%s", err, res.Description)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				logger.Error("绘画任务执行失败:", errMsg)
 | 
			
		||||
				job.Progress = service.FailTaskProgress
 | 
			
		||||
				job.ErrMsg = errMsg
 | 
			
		||||
				// update the task progress
 | 
			
		||||
				s.db.Updates(&job)
 | 
			
		||||
				// 任务失败,通知前端
 | 
			
		||||
				s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: int(job.Id), Message: service.TaskStatusFailed})
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			logger.Infof("任务提交成功:%+v", res)
 | 
			
		||||
			// 更新任务 ID/频道
 | 
			
		||||
			job.TaskId = res.Result
 | 
			
		||||
			job.MessageId = res.Result
 | 
			
		||||
			job.ChannelId = res.Channel
 | 
			
		||||
			s.db.Updates(&job)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CBReq struct {
 | 
			
		||||
	Id          string      `json:"id"`
 | 
			
		||||
	Action      string      `json:"action"`
 | 
			
		||||
	Status      string      `json:"status"`
 | 
			
		||||
	Prompt      string      `json:"prompt"`
 | 
			
		||||
	PromptEn    string      `json:"promptEn"`
 | 
			
		||||
	Description string      `json:"description"`
 | 
			
		||||
	SubmitTime  int64       `json:"submitTime"`
 | 
			
		||||
	StartTime   int64       `json:"startTime"`
 | 
			
		||||
	FinishTime  int64       `json:"finishTime"`
 | 
			
		||||
	Progress    string      `json:"progress"`
 | 
			
		||||
	ImageUrl    string      `json:"imageUrl"`
 | 
			
		||||
	FailReason  interface{} `json:"failReason"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
		FinalPrompt string `json:"finalPrompt"`
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetImageHash(action string) string {
 | 
			
		||||
	split := strings.Split(action, "::")
 | 
			
		||||
	if len(split) > 5 {
 | 
			
		||||
		return split[4]
 | 
			
		||||
	}
 | 
			
		||||
	return split[len(split)-1]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) CheckTaskNotify() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			var message service.NotifyMessage
 | 
			
		||||
			err := s.notifyQueue.LPop(&message)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			logger.Debugf("receive a new mj notify message: %+v", message)
 | 
			
		||||
			client := s.wsService.Clients.Get(message.ClientId)
 | 
			
		||||
			if client == nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			utils.SendChannelMsg(client, types.ChMj, message.Message)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) DownloadImages() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		var items []model.MidJourneyJob
 | 
			
		||||
		for {
 | 
			
		||||
			res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
 | 
			
		||||
			if res.Error != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// download images
 | 
			
		||||
			for _, v := range items {
 | 
			
		||||
				if v.OrgURL == "" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				logger.Infof("try to download image: %s", v.OrgURL)
 | 
			
		||||
				// 如果是返回的是 discord 图片地址,则使用代理下载
 | 
			
		||||
				proxy := false
 | 
			
		||||
				if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
 | 
			
		||||
					proxy = true
 | 
			
		||||
				}
 | 
			
		||||
				imgURL, err := s.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy)
 | 
			
		||||
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Errorf("error with download image %s, %v", v.OrgURL, err)
 | 
			
		||||
					continue
 | 
			
		||||
				} else {
 | 
			
		||||
					logger.Infof("download image %s successfully.", v.OrgURL)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				v.ImgURL = imgURL
 | 
			
		||||
				s.db.Updates(&v)
 | 
			
		||||
 | 
			
		||||
				s.notifyQueue.RPush(service.NotifyMessage{
 | 
			
		||||
					ClientId: s.clientIds[v.Id],
 | 
			
		||||
					UserId:   v.UserId,
 | 
			
		||||
					JobId:    int(v.Id),
 | 
			
		||||
					Message:  service.TaskStatusFinished})
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			time.Sleep(time.Second * 5)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushTask push a new mj task in to task queue
 | 
			
		||||
func (s *Service) PushTask(task types.MjTask) {
 | 
			
		||||
	logger.Debugf("add a new MidJourney task to the task list: %+v", task)
 | 
			
		||||
	s.taskQueue.RPush(task)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SyncTaskProgress 异步拉取任务
 | 
			
		||||
func (s *Service) SyncTaskProgress() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		var jobs []model.MidJourneyJob
 | 
			
		||||
		for {
 | 
			
		||||
			err := s.db.Where("progress < ?", 100).Find(&jobs).Error
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, job := range jobs {
 | 
			
		||||
				// 10 分钟还没完成的任务标记为失败
 | 
			
		||||
				if time.Now().Sub(job.CreatedAt) > time.Minute*10 {
 | 
			
		||||
					job.Progress = service.FailTaskProgress
 | 
			
		||||
					job.ErrMsg = "任务超时"
 | 
			
		||||
					s.db.Updates(&job)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if job.ChannelId == "" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				task, err := s.client.QueryTask(job.TaskId, job.ChannelId)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Errorf("error with query task: %v", err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 任务执行失败了
 | 
			
		||||
				if task.FailReason != "" {
 | 
			
		||||
					s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
 | 
			
		||||
						"progress": service.FailTaskProgress,
 | 
			
		||||
						"err_msg":  task.FailReason,
 | 
			
		||||
					})
 | 
			
		||||
					logger.Errorf("task failed: %v", task.FailReason)
 | 
			
		||||
					s.notifyQueue.RPush(service.NotifyMessage{
 | 
			
		||||
						ClientId: s.clientIds[job.Id],
 | 
			
		||||
						UserId:   job.UserId,
 | 
			
		||||
						JobId:    int(job.Id),
 | 
			
		||||
						Message:  service.TaskStatusFailed})
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if len(task.Buttons) > 0 {
 | 
			
		||||
					job.Hash = GetImageHash(task.Buttons[0].CustomId)
 | 
			
		||||
				}
 | 
			
		||||
				oldProgress := job.Progress
 | 
			
		||||
				job.Progress = utils.IntValue(strings.Replace(task.Progress, "%", "", 1), 0)
 | 
			
		||||
				if task.ImageUrl != "" {
 | 
			
		||||
					job.OrgURL = task.ImageUrl
 | 
			
		||||
				}
 | 
			
		||||
				err = s.db.Updates(&job).Error
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Errorf("error with update database: %v", err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 通知前端更新任务进度
 | 
			
		||||
				if oldProgress != job.Progress {
 | 
			
		||||
					message := service.TaskStatusRunning
 | 
			
		||||
					if job.Progress == 100 {
 | 
			
		||||
						message = service.TaskStatusFinished
 | 
			
		||||
					}
 | 
			
		||||
					s.notifyQueue.RPush(service.NotifyMessage{
 | 
			
		||||
						ClientId: s.clientIds[job.Id],
 | 
			
		||||
						UserId:   job.UserId,
 | 
			
		||||
						JobId:    int(job.Id),
 | 
			
		||||
						Message:  message})
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 找出失败的任务,并恢复其扣减算力
 | 
			
		||||
			s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs)
 | 
			
		||||
			for _, job := range jobs {
 | 
			
		||||
				err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
 | 
			
		||||
					Type:   types.PowerRefund,
 | 
			
		||||
					Model:  "mid-journey",
 | 
			
		||||
					Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg),
 | 
			
		||||
				})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				// 更新任务状态
 | 
			
		||||
				s.db.Model(&job).UpdateColumn("power", 0)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			time.Sleep(time.Second * 5)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										137
									
								
								api/service/oss/aliyun_oss.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								api/service/oss/aliyun_oss.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
package oss
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aliyun/aliyun-oss-go-sdk/oss"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AliYunOss struct {
 | 
			
		||||
	config   *types.AliYunOssConfig
 | 
			
		||||
	bucket   *oss.Bucket
 | 
			
		||||
	proxyURL string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAliYunOss(appConfig *types.AppConfig) (*AliYunOss, error) {
 | 
			
		||||
	config := &appConfig.OSS.AliYun
 | 
			
		||||
	// 创建 OSS 客户端
 | 
			
		||||
	client, err := oss.New(config.Endpoint, config.AccessKey, config.AccessSecret)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 获取存储空间
 | 
			
		||||
	bucket, err := client.Bucket(config.Bucket)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.SubDir == "" {
 | 
			
		||||
		config.SubDir = "gpt"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &AliYunOss{
 | 
			
		||||
		config:   config,
 | 
			
		||||
		bucket:   bucket,
 | 
			
		||||
		proxyURL: appConfig.ProxyURL,
 | 
			
		||||
	}, nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {
 | 
			
		||||
	// 解析表单
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
	// 打开上传文件
 | 
			
		||||
	src, err := file.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
	defer src.Close()
 | 
			
		||||
 | 
			
		||||
	fileExt := filepath.Ext(file.Filename)
 | 
			
		||||
	objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	// 上传文件
 | 
			
		||||
	err = s.bucket.PutObject(objectKey, src)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return File{
 | 
			
		||||
		Name:   file.Filename,
 | 
			
		||||
		ObjKey: objectKey,
 | 
			
		||||
		URL:    fmt.Sprintf("%s/%s", s.config.Domain, objectKey),
 | 
			
		||||
		Ext:    fileExt,
 | 
			
		||||
		Size:   file.Size,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
 | 
			
		||||
	var fileData []byte
 | 
			
		||||
	var err error
 | 
			
		||||
	if useProxy {
 | 
			
		||||
		fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
 | 
			
		||||
	} else {
 | 
			
		||||
		fileData, err = utils.DownloadImage(fileURL, "")
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with download image: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	parse, err := url.Parse(fileURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with parse image URL: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	fileExt := utils.GetImgExt(parse.Path)
 | 
			
		||||
	objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	// 上传文件字节数据
 | 
			
		||||
	err = s.bucket.PutObject(objectKey, bytes.NewReader(fileData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s/%s", s.config.Domain, objectKey), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s AliYunOss) PutBase64(base64Img string) (string, error) {
 | 
			
		||||
	imageData, err := base64.StdEncoding.DecodeString(base64Img)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error decoding base64:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
	objectKey := fmt.Sprintf("%s/%d.png", s.config.SubDir, time.Now().UnixMicro())
 | 
			
		||||
	// 上传文件字节数据
 | 
			
		||||
	err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s/%s", s.config.Domain, objectKey), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s AliYunOss) Delete(fileURL string) error {
 | 
			
		||||
	var objectKey string
 | 
			
		||||
	if strings.HasPrefix(fileURL, "http") {
 | 
			
		||||
		filename := filepath.Base(fileURL)
 | 
			
		||||
		objectKey = fmt.Sprintf("%s/%s", s.config.SubDir, filename)
 | 
			
		||||
	} else {
 | 
			
		||||
		objectKey = fileURL
 | 
			
		||||
	}
 | 
			
		||||
	return s.bucket.DeleteObject(objectKey)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Uploader = AliYunOss{}
 | 
			
		||||
							
								
								
									
										105
									
								
								api/service/oss/localstorage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								api/service/oss/localstorage.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
package oss
 | 
			
		||||
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
// * 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/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LocalStorage struct {
 | 
			
		||||
	config   *types.LocalStorageConfig
 | 
			
		||||
	proxyURL string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLocalStorage(config *types.AppConfig) LocalStorage {
 | 
			
		||||
	return LocalStorage{
 | 
			
		||||
		config:   &config.OSS.Local,
 | 
			
		||||
		proxyURL: config.ProxyURL,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return File{}, fmt.Errorf("error with get form: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	path, err := utils.GenUploadPath(s.config.BasePath, file.Filename, false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return File{}, fmt.Errorf("error with generate filename: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	// 将文件保存到指定路径
 | 
			
		||||
	err = ctx.SaveUploadedFile(file, path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return File{}, fmt.Errorf("error with save upload file: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ext := filepath.Ext(file.Filename)
 | 
			
		||||
	return File{
 | 
			
		||||
		Name:   file.Filename,
 | 
			
		||||
		ObjKey: path,
 | 
			
		||||
		URL:    utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, path),
 | 
			
		||||
		Ext:    ext,
 | 
			
		||||
		Size:   file.Size,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorage) PutUrlFile(fileURL string, useProxy bool) (string, error) {
 | 
			
		||||
	parse, err := url.Parse(fileURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with parse image URL: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	filename := filepath.Base(parse.Path)
 | 
			
		||||
	filePath, err := utils.GenUploadPath(s.config.BasePath, filename, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with generate image dir: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if useProxy {
 | 
			
		||||
		err = utils.DownloadFile(fileURL, filePath, s.proxyURL)
 | 
			
		||||
	} else {
 | 
			
		||||
		err = utils.DownloadFile(fileURL, filePath, "")
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with download image: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorage) PutBase64(base64Img string) (string, error) {
 | 
			
		||||
	imageData, err := base64.StdEncoding.DecodeString(base64Img)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error decoding base64:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
	filePath, err := utils.GenUploadPath(s.config.BasePath, "", true)
 | 
			
		||||
	err = os.WriteFile(filePath, imageData, 0644)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error writing to file:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorage) Delete(fileURL string) error {
 | 
			
		||||
	if _, err := os.Stat(fileURL); err == nil {
 | 
			
		||||
		return os.Remove(fileURL)
 | 
			
		||||
	}
 | 
			
		||||
	filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
 | 
			
		||||
	return os.Remove(filePath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Uploader = LocalStorage{}
 | 
			
		||||
@@ -1,64 +0,0 @@
 | 
			
		||||
package oss
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LocalStorageService struct {
 | 
			
		||||
	config   *types.LocalStorageConfig
 | 
			
		||||
	proxyURL string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLocalStorageService(config *types.AppConfig) LocalStorageService {
 | 
			
		||||
	return LocalStorageService{
 | 
			
		||||
		config:   &config.OSS.Local,
 | 
			
		||||
		proxyURL: config.ProxyURL,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorageService) PutFile(ctx *gin.Context, name string) (string, error) {
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with get form: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filePath, err := utils.GenUploadPath(s.config.BasePath, file.Filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with generate filename: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	// 将文件保存到指定路径
 | 
			
		||||
	err = ctx.SaveUploadedFile(file, filePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with save upload file: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorageService) PutImg(imageURL string) (string, error) {
 | 
			
		||||
	filename := filepath.Base(imageURL)
 | 
			
		||||
	filePath, err := utils.GenUploadPath(s.config.BasePath, filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with generate image dir: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with download image: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorageService) Delete(fileURL string) error {
 | 
			
		||||
	filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
 | 
			
		||||
	return os.Remove(filePath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Uploader = LocalStorageService{}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user