mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Compare commits
	
		
			2458 Commits
		
	
	
		
			v2.0.1
			...
			d3904f2166
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d3904f2166 | ||
| 
						 | 
					3809375694 | ||
| 
						 | 
					1b0de25986 | ||
| 
						 | 
					865c45dd29 | ||
| 
						 | 
					1f5d8e6d9c | ||
| 
						 | 
					c9ef6d58ed | ||
| 
						 | 
					2d7229d2b8 | ||
| 
						 | 
					11b37c15bd | ||
| 
						 | 
					1d0038f17d | ||
| 
						 | 
					619fa519c0 | ||
| 
						 | 
					48469bd8ca | ||
| 
						 | 
					5a5e887f2b | ||
| 
						 | 
					b6f5d75656 | ||
| 
						 | 
					0d41a17ef6 | ||
| 
						 | 
					f7cde17919 | ||
| 
						 | 
					570cbb34b6 | ||
| 
						 | 
					7aa9ae0a3e | ||
| 
						 | 
					2d4180f5be | ||
| 
						 | 
					9f0182b55e | ||
| 
						 | 
					ad6666eeaf | ||
| 
						 | 
					a2c4e468a0 | ||
| 
						 | 
					2167076652 | ||
| 
						 | 
					e123076250 | ||
| 
						 | 
					ebcb4db245 | ||
| 
						 | 
					0a25a1a8cb | ||
| 
						 | 
					f3154b20a5 | ||
| 
						 | 
					b709ee3983 | ||
| 
						 | 
					f5f3ce94f6 | ||
| 
						 | 
					2b5f600308 | ||
| 
						 | 
					b966107117 | ||
| 
						 | 
					377480b448 | ||
| 
						 | 
					8bd0d6a1a7 | ||
| 
						 | 
					90827fc593 | ||
| 
						 | 
					008e339b6d | ||
| 
						 | 
					ebf84a3b2c | ||
| 
						 | 
					fd796521d4 | ||
| 
						 | 
					6a25bdcf2d | ||
| 
						 | 
					a899e9aceb | ||
| 
						 | 
					c0826d2275 | ||
| 
						 | 
					12863f5213 | ||
| 
						 | 
					cf140d4228 | ||
| 
						 | 
					476d946f96 | ||
| 
						 | 
					9714258322 | ||
| 
						 | 
					48cd4b11b5 | ||
| 
						 | 
					77c78b230a | ||
| 
						 | 
					b44686b887 | ||
| 
						 | 
					34bdd4b945 | ||
| 
						 | 
					b0758cccde | ||
| 
						 | 
					98a11e56d2 | ||
| 
						 | 
					86f86962fb | ||
| 
						 | 
					2137aa65bf | ||
| 
						 | 
					18fa2cc30d | ||
| 
						 | 
					0bfc648085 | ||
| 
						 | 
					9f91c2d05c | ||
| 
						 | 
					a029b4330b | ||
| 
						 | 
					2842b264e0 | ||
| 
						 | 
					c2edfec16f | ||
| 
						 | 
					6406ac99a3 | ||
| 
						 | 
					97a4aafc92 | ||
| 
						 | 
					d8f533e1f3 | ||
| 
						 | 
					c6199dbf9f | ||
| 
						 | 
					4273aa0803 | ||
| 
						 | 
					acf75ce68f | ||
| 
						 | 
					1ae5fdbf01 | ||
| 
						 | 
					2a3996e0d6 | ||
| 
						 | 
					fdbaddde37 | ||
| 
						 | 
					d74f79e9c5 | ||
| 
						 | 
					c4e9cb03a9 | ||
| 
						 | 
					bf265d3375 | ||
| 
						 | 
					17f391d929 | ||
| 
						 | 
					78186c27fb | ||
| 
						 | 
					a5a9768245 | ||
| 
						 | 
					3fe55b4f7f | ||
| 
						 | 
					f156430cc5 | ||
| 
						 | 
					f30c6a4348 | ||
| 
						 | 
					a780b39c17 | ||
| 
						 | 
					1010db834c | ||
| 
						 | 
					51384ddc5f | ||
| 
						 | 
					e5e5fde924 | ||
| 
						 | 
					add9ca200c | ||
| 
						 | 
					5225a6e192 | ||
| 
						 | 
					28cbe56cec | ||
| 
						 | 
					ad9ab9d45a | ||
| 
						 | 
					bb4832e6e7 | ||
| 
						 | 
					39b3487ea0 | ||
| 
						 | 
					32b60909ae | ||
| 
						 | 
					5db6775cb8 | ||
| 
						 | 
					b6881c7797 | ||
| 
						 | 
					9943a52295 | ||
| 
						 | 
					1db4d25370 | ||
| 
						 | 
					92f57fb18f | ||
| 
						 | 
					4c4d44e2f8 | ||
| 
						 | 
					8f12beb8f0 | ||
| 
						 | 
					2e7cac3218 | ||
| 
						 | 
					60fa358010 | ||
| 
						 | 
					034b7d4655 | ||
| 
						 | 
					1e20b64048 | ||
| 
						 | 
					4f28fca506 | ||
| 
						 | 
					3ef5993085 | ||
| 
						 | 
					09ad7c1875 | ||
| 
						 | 
					31e52cb47e | ||
| 
						 | 
					9a69c5bd7c | ||
| 
						 | 
					be645aab37 | ||
| 
						 | 
					c41e86faa6 | ||
| 
						 | 
					143be69a7f | ||
| 
						 | 
					63b7626656 | ||
| 
						 | 
					dabb7c70d5 | ||
| 
						 | 
					c449737127 | ||
| 
						 | 
					553b8c9f28 | ||
| 
						 | 
					19314793b8 | ||
| 
						 | 
					8680182921 | ||
| 
						 | 
					2173c82bb5 | ||
| 
						 | 
					0d5e66a9ae | ||
| 
						 | 
					2f9cb5a68f | ||
| 
						 | 
					55cacfb7e2 | ||
| 
						 | 
					6a862372f7 | ||
| 
						 | 
					81bd83eb44 | ||
| 
						 | 
					b2b6fd81be | ||
| 
						 | 
					f22cfd7b33 | ||
| 
						 | 
					8111acff34 | ||
| 
						 | 
					4cad55379d | ||
| 
						 | 
					a3d3ce3f4c | ||
| 
						 | 
					611e97e641 | ||
| 
						 | 
					bfeea4ed49 | ||
| 
						 | 
					bc71ae247b | ||
| 
						 | 
					0112b54bc7 | ||
| 
						 | 
					65810d918b | ||
| 
						 | 
					4d535b1cd0 | ||
| 
						 | 
					588d81e8f1 | ||
| 
						 | 
					d4f499ee41 | ||
| 
						 | 
					4d63d73b2e | ||
| 
						 | 
					07c63497dc | ||
| 
						 | 
					e440ff56c8 | ||
| 
						 | 
					c89e4883b2 | ||
| 
						 | 
					ac3d940de8 | ||
| 
						 | 
					be59de56f0 | ||
| 
						 | 
					a70e9a3c01 | ||
| 
						 | 
					8aa9a500fd | ||
| 
						 | 
					93652db688 | ||
| 
						 | 
					8421c483e8 | ||
| 
						 | 
					4ac27fdd4d | ||
| 
						 | 
					b6b2c501fd | ||
| 
						 | 
					ce13cf61a7 | ||
| 
						 | 
					a3af563e89 | ||
| 
						 | 
					e95c94d7be | ||
| 
						 | 
					125a71fead | ||
| 
						 | 
					b410ec399c | ||
| 
						 | 
					7d51bfd42e | ||
| 
						 | 
					0c14ce6417 | ||
| 
						 | 
					f2a2b40d2c | ||
| 
						 | 
					77be190d76 | ||
| 
						 | 
					c56587c438 | ||
| 
						 | 
					840c151ab9 | ||
| 
						 | 
					0af04e0f2f | ||
| 
						 | 
					d184eb6458 | ||
| 
						 | 
					c5d9b1131e | ||
| 
						 | 
					e13408dd24 | ||
| 
						 | 
					aba4baf384 | ||
| 
						 | 
					6d84f9d3ae | ||
| 
						 | 
					63c5baaa80 | ||
| 
						 | 
					defefba925 | ||
| 
						 | 
					90c531c224 | ||
| 
						 | 
					266e9efd2e | ||
| 
						 | 
					57c88c0717 | ||
| 
						 | 
					5b5dea1c59 | ||
| 
						 | 
					d56566cd73 | ||
| 
						 | 
					b5d104c908 | ||
| 
						 | 
					f9e9129d52 | ||
| 
						 | 
					2a8a18391e | ||
| 
						 | 
					e1cb8e36fa | ||
| 
						 | 
					b948d6bf86 | ||
| 
						 | 
					fe67f79050 | ||
| 
						 | 
					67338ff9b7 | ||
| 
						 | 
					7380c8a2c1 | ||
| 
						 | 
					e1ba8f1b0f | ||
| 
						 | 
					c0062ff280 | ||
| 
						 | 
					39e593da48 | ||
| 
						 | 
					f8b10ad8b1 | ||
| 
						 | 
					8a22c9d6db | ||
| 
						 | 
					5f96804f3b | ||
| 
						 | 
					13430ea3e2 | ||
| 
						 | 
					664879b9df | ||
| 
						 | 
					9df24e568b | ||
| 
						 | 
					bc322be448 | ||
| 
						 | 
					a867adaf04 | ||
| 
						 | 
					0cb186846a | ||
| 
						 | 
					e467ce028d | ||
| 
						 | 
					cdfe907fb5 | ||
| 
						 | 
					d91af7f983 | ||
| 
						 | 
					c3108ad333 | ||
| 
						 | 
					081daf937e | ||
| 
						 | 
					0c3d4462ca | ||
| 
						 | 
					3c859fc29f | ||
| 
						 | 
					e1c7c54dfa | ||
| 
						 | 
					87b5e3bf62 | ||
| 
						 | 
					1d15666713 | ||
| 
						 | 
					a127ae1fb4 | ||
| 
						 | 
					ea1329f73e | ||
| 
						 | 
					149d732cb7 | ||
| 
						 | 
					210b29bfbe | ||
| 
						 | 
					acc2e97aab | ||
| 
						 | 
					93ac0e5017 | ||
| 
						 | 
					ed8c3580c8 | ||
| 
						 | 
					0a056a7c5c | ||
| 
						 | 
					74c4711cdd | ||
| 
						 | 
					eceec092cf | ||
| 
						 | 
					42743410a8 | ||
| 
						 | 
					0f04756d4c | ||
| 
						 | 
					acdded8161 | ||
| 
						 | 
					e939ce5a02 | ||
| 
						 | 
					46a0b100f7 | ||
| 
						 | 
					e27e8fb0e1 | ||
| 
						 | 
					93c5320bf2 | ||
| 
						 | 
					a433d1606c | ||
| 
						 | 
					cc5e16b045 | ||
| 
						 | 
					54f6feb2d7 | ||
| 
						 | 
					e1ac0538b8 | ||
| 
						 | 
					1a678cb4d8 | ||
| 
						 | 
					83cea3a90d | ||
| 
						 | 
					759a09a76c | ||
| 
						 | 
					2623a92763 | ||
| 
						 | 
					3932c594c7 | ||
| 
						 | 
					b7acb89096 | ||
| 
						 | 
					ef24d3e633 | ||
| 
						 | 
					23350c842b | ||
| 
						 | 
					a2adfbbd32 | ||
| 
						 | 
					f22cec1eb4 | ||
| 
						 | 
					e56216549e | ||
| 
						 | 
					19facc7c85 | ||
| 
						 | 
					b08ce5630c | ||
| 
						 | 
					b41c012d27 | ||
| 
						 | 
					a392daab71 | ||
| 
						 | 
					0628ddfc6f | ||
| 
						 | 
					7eda14f138 | ||
| 
						 | 
					9a86c42c95 | ||
| 
						 | 
					819d249a09 | ||
| 
						 | 
					8d66fedb1f | ||
| 
						 | 
					7cf89b53ce | ||
| 
						 | 
					459c373f13 | ||
| 
						 | 
					1d14a991ee | ||
| 
						 | 
					05ef5adfa7 | ||
| 
						 | 
					38fa3056df | ||
| 
						 | 
					289aeec8af | ||
| 
						 | 
					7d71da938f | ||
| 
						 | 
					f8f6954115 | ||
| 
						 | 
					6e03f32871 | ||
| 
						 | 
					18a6571883 | ||
| 
						 | 
					14f444e1f0 | ||
| 
						 | 
					2b0f2e5f9d | ||
| 
						 | 
					4629b39c29 | ||
| 
						 | 
					d33e772fa5 | ||
| 
						 | 
					89136fba32 | ||
| 
						 | 
					8b4ca133fd | ||
| 
						 | 
					a4c9eaf6cd | ||
| 
						 | 
					50e63109a3 | ||
| 
						 | 
					48a1e8a584 | ||
| 
						 | 
					e44ebe3f0e | ||
| 
						 | 
					108069a0c6 | ||
| 
						 | 
					d5bda2904d | ||
| 
						 | 
					283caba8ce | ||
| 
						 | 
					b78e5db817 | ||
| 
						 | 
					46c469b2d7 | ||
| 
						 | 
					c00ebbea4f | ||
| 
						 | 
					c526ff80b5 | ||
| 
						 | 
					0037b0c944 | ||
| 
						 | 
					6f81bb3b8a | ||
| 
						 | 
					7bdc45ed3e | ||
| 
						 | 
					88cd3ac122 | ||
| 
						 | 
					4988d2ee26 | ||
| 
						 | 
					8deb7a92ee | ||
| 
						 | 
					db060d732a | ||
| 
						 | 
					522627820a | ||
| 
						 | 
					cf46d5ad63 | ||
| 
						 | 
					a4941521d0 | ||
| 
						 | 
					f6e1f8398b | ||
| 
						 | 
					d544eead38 | ||
| 
						 | 
					fbb9385f23 | ||
| 
						 | 
					18144c3d9c | ||
| 
						 | 
					64aa760e58 | ||
| 
						 | 
					e0bbb8bb68 | ||
| 
						 | 
					6667ee1c7f | ||
| 
						 | 
					6ded4e96e7 | ||
| 
						 | 
					85cdcab850 | ||
| 
						 | 
					f4c9410c29 | ||
| 
						 | 
					adf7d8200b | ||
| 
						 | 
					3086a2fa77 | ||
| 
						 | 
					f526d6f560 | ||
| 
						 | 
					106461a1e7 | ||
| 
						 | 
					c4e19dbc59 | ||
| 
						 | 
					f3603e59fa | ||
| 
						 | 
					8e2484fcdf | ||
| 
						 | 
					00d6cb27f7 | ||
| 
						 | 
					b844045d23 | ||
| 
						 | 
					e49fe976d9 | ||
| 
						 | 
					14f751965f | ||
| 
						 | 
					0ec423389f | ||
| 
						 | 
					820ab54e2d | ||
| 
						 | 
					a6c1eb27a8 | ||
| 
						 | 
					0dc4071ccc | ||
| 
						 | 
					4d3949718a | ||
| 
						 | 
					aef535f1a7 | ||
| 
						 | 
					686a80e727 | ||
| 
						 | 
					e49466fa05 | ||
| 
						 | 
					4b93370814 | ||
| 
						 | 
					5733e3c588 | ||
| 
						 | 
					44fc5b5cbf | ||
| 
						 | 
					2d3f7c922f | ||
| 
						 | 
					fe8cca3730 | ||
| 
						 | 
					fbb7a1e853 | ||
| 
						 | 
					fb2c15567d | ||
| 
						 | 
					c2c52a1f60 | ||
| 
						 | 
					106ddc17cd | ||
| 
						 | 
					17d5209738 | ||
| 
						 | 
					d66bfc6352 | ||
| 
						 | 
					4d75b23ed1 | ||
| 
						 | 
					36bfa2ef7c | ||
| 
						 | 
					afe12c212e | ||
| 
						 | 
					adf97c6d8b | ||
| 
						 | 
					7a8d557ea3 | ||
| 
						 | 
					d3f0a77830 | ||
| 
						 | 
					0581e37236 | ||
| 
						 | 
					44383a8b33 | ||
| 
						 | 
					7c466c9b9c | ||
| 
						 | 
					a0fa4d7e72 | ||
| 
						 | 
					d357b45e84 | ||
| 
						 | 
					d0bd1bf8fd | ||
| 
						 | 
					86ffa1e643 | ||
| 
						 | 
					b0d28eb77e | ||
| 
						 | 
					736cbdbdd1 | ||
| 
						 | 
					613d67eada | ||
| 
						 | 
					89cea18955 | ||
| 
						 | 
					56bc77d20b | ||
| 
						 | 
					6d93d37963 | ||
| 
						 | 
					24df85cf9d | ||
| 
						 | 
					a4d7a2c6e3 | ||
| 
						 | 
					49d42bb45d | ||
| 
						 | 
					4f49626303 | ||
| 
						 | 
					45db20c1c3 | ||
| 
						 | 
					82994843f5 | ||
| 
						 | 
					1110a087a0 | ||
| 
						 | 
					f0b3e10a6c | ||
| 
						 | 
					f89872b833 | ||
| 
						 | 
					90ced92876 | ||
| 
						 | 
					2c74559010 | ||
| 
						 | 
					e3ca7e8b44 | ||
| 
						 | 
					4745706c42 | ||
| 
						 | 
					801dc412f9 | ||
| 
						 | 
					c7c2c0211a | ||
| 
						 | 
					65bb962fc0 | ||
| 
						 | 
					e791cd441d | ||
| 
						 | 
					8455fefc8a | ||
| 
						 | 
					06f897f32f | ||
| 
						 | 
					deb1e76c41 | ||
| 
						 | 
					463fa743e9 | ||
| 
						 | 
					cda4494cec | ||
| 
						 | 
					87d85c10c3 | ||
| 
						 | 
					22f83c9e11 | ||
| 
						 | 
					7f454cbcec | ||
| 
						 | 
					426269d795 | ||
| 
						 | 
					370f143157 | ||
| 
						 | 
					103106bb93 | ||
| 
						 | 
					2419083adf | ||
| 
						 | 
					c25903bfb4 | ||
| 
						 | 
					e34c266438 | ||
| 
						 | 
					8c39a687b5 | ||
| 
						 | 
					592f62005b | ||
| 
						 | 
					12e7caa209 | ||
| 
						 | 
					b016771555 | ||
| 
						 | 
					a84383f919 | ||
| 
						 | 
					7f68fb1ff2 | ||
| 
						 | 
					8d2003fe68 | ||
| 
						 | 
					9961b513cc | ||
| 
						 | 
					819238acaf | ||
| 
						 | 
					ad49916b1c | ||
| 
						 | 
					d18bd8a48a | ||
| 
						 | 
					4a1319f2c0 | ||
| 
						 | 
					8fd843d228 | ||
| 
						 | 
					6792d6e475 | ||
| 
						 | 
					c139038e01 | ||
| 
						 | 
					4a7fd3a380 | ||
| 
						 | 
					c98dc31cdf | ||
| 
						 | 
					bd43af3a8d | ||
| 
						 | 
					be98aa2078 | ||
| 
						 | 
					a0d4a04192 | ||
| 
						 | 
					bd9de4dc4d | ||
| 
						 | 
					2eebfcf6fe | ||
| 
						 | 
					c5074f0aa4 | ||
| 
						 | 
					ba58018a15 | ||
| 
						 | 
					63ab83c3c8 | ||
| 
						 | 
					268cf3b606 | ||
| 
						 | 
					fbc68fa776 | ||
| 
						 | 
					4ae34ea3ee | ||
| 
						 | 
					96273fd75e | ||
| 
						 | 
					3e63d405c1 | ||
| 
						 | 
					19b42aac5d | ||
| 
						 | 
					b67a23200e | ||
| 
						 | 
					1dac02e4d6 | ||
| 
						 | 
					acad5b1d08 | ||
| 
						 | 
					4e9bb51d2f | ||
| 
						 | 
					c0c8cdbbf3 | ||
| 
						 | 
					cbdc611b54 | ||
| 
						 | 
					93ca303b6c | ||
| 
						 | 
					a925b424a8 | ||
| 
						 | 
					5b4d423b58 | ||
| 
						 | 
					6c1cbe120c | ||
| 
						 | 
					77a58bc4b0 | ||
| 
						 | 
					7d55a6d0e4 | ||
| 
						 | 
					8ad63a6c25 | ||
| 
						 | 
					acf9fa36f9 | ||
| 
						 | 
					461154bb03 | ||
| 
						 | 
					cd75461f9e | ||
| 
						 | 
					2bac174e6f | ||
| 
						 | 
					65f80f81ad | ||
| 
						 | 
					450766a44b | ||
| 
						 | 
					05e6e4bffb | ||
| 
						 | 
					fbb66a4a5d | ||
| 
						 | 
					d51d31a559 | ||
| 
						 | 
					919ee51dca | ||
| 
						 | 
					9c577ad9d5 | ||
| 
						 | 
					953114041b | ||
| 
						 | 
					d830c23dab | ||
| 
						 | 
					fd3568c459 | ||
| 
						 | 
					3029dcb2f6 | ||
| 
						 | 
					35e03e1bca | ||
| 
						 | 
					cea5b91f96 | ||
| 
						 | 
					d2984db6e7 | ||
| 
						 | 
					deb215ccd1 | ||
| 
						 | 
					7173cf2184 | ||
| 
						 | 
					0c697e123d | ||
| 
						 | 
					edfa6d14ee | ||
| 
						 | 
					b6d9ba93fa | ||
| 
						 | 
					6293b95a3b | ||
| 
						 | 
					ef4665cd8b | ||
| 
						 | 
					8030e71a5a | ||
| 
						 | 
					f42488d4cb | ||
| 
						 | 
					af49ed4fdc | ||
| 
						 | 
					b174a40634 | ||
| 
						 | 
					3c01738c29 | ||
| 
						 | 
					9be58f3eb4 | ||
| 
						 | 
					a50c282d01 | ||
| 
						 | 
					5141145e4d | ||
| 
						 | 
					b5f6e5a598 | ||
| 
						 | 
					7df308d655 | ||
| 
						 | 
					f5ad51a35e | ||
| 
						 | 
					f9d4105170 | ||
| 
						 | 
					9e6ee50fa6 | ||
| 
						 | 
					dd77ad5d74 | ||
| 
						 | 
					3898c507c4 | ||
| 
						 | 
					fcba50f041 | ||
| 
						 | 
					452fc86ad1 | ||
| 
						 | 
					5bdf411399 | ||
| 
						 | 
					2d920f7ccc | ||
| 
						 | 
					d84d51b475 | ||
| 
						 | 
					f9d6f4f9da | ||
| 
						 | 
					a13bd624e8 | ||
| 
						 | 
					8fb019b2e2 | ||
| 
						 | 
					2f3457e73d | ||
| 
						 | 
					c6ebd6e73c | ||
| 
						 | 
					2333a47c55 | ||
| 
						 | 
					b35895b551 | ||
| 
						 | 
					19c4ed4463 | ||
| 
						 | 
					22aa1698b4 | ||
| 
						 | 
					07d089a2bd | ||
| 
						 | 
					870ad913cc | ||
| 
						 | 
					3fb389551b | ||
| 
						 | 
					d12a4adfb5 | ||
| 
						 | 
					702e17c96b | ||
| 
						 | 
					93ff7d26cc | ||
| 
						 | 
					13777786c4 | ||
| 
						 | 
					6655c64e55 | ||
| 
						 | 
					7f3f6f1aaf | ||
| 
						 | 
					ea04595c5e | ||
| 
						 | 
					13c68bd810 | ||
| 
						 | 
					1d2f44fba8 | ||
| 
						 | 
					68702bfb1f | ||
| 
						 | 
					e83f61e74d | ||
| 
						 | 
					10d472e79e | ||
| 
						 | 
					a6b920d9af | ||
| 
						 | 
					064e964d75 | ||
| 
						 | 
					47fb40d572 | ||
| 
						 | 
					b7892b58f5 | ||
| 
						 | 
					77f037a3c4 | ||
| 
						 | 
					248d27680d | ||
| 
						 | 
					4c84182e7a | ||
| 
						 | 
					9e18cc260b | ||
| 
						 | 
					e8581c8f3c | ||
| 
						 | 
					fe4cba8baf | ||
| 
						 | 
					9bbd7d3185 | ||
| 
						 | 
					dbabb2c403 | ||
| 
						 | 
					6c37d04591 | ||
| 
						 | 
					649c5be64e | ||
| 
						 | 
					fc0042a799 | ||
| 
						 | 
					269d064e0a | ||
| 
						 | 
					6c8143b7de | ||
| 
						 | 
					f9f99639db | ||
| 
						 | 
					6d5bf490ab | ||
| 
						 | 
					46fc2a5012 | ||
| 
						 | 
					90e7b5aecf | ||
| 
						 | 
					ed20fd2962 | ||
| 
						 | 
					4c3fd55a75 | ||
| 
						 | 
					d95d509046 | ||
| 
						 | 
					c15c852668 | ||
| 
						 | 
					4a60512ae7 | ||
| 
						 | 
					0e210cf8de | ||
| 
						 | 
					35aa2c7270 | ||
| 
						 | 
					518e0d90a5 | ||
| 
						 | 
					51f7b02b27 | ||
| 
						 | 
					23f2b6213c | ||
| 
						 | 
					3a969054e3 | ||
| 
						 | 
					4d1f9e49d4 | ||
| 
						 | 
					702f5bd362 | ||
| 
						 | 
					2474d5b6d2 | ||
| 
						 | 
					9858d1f958 | ||
| 
						 | 
					62efab987b | ||
| 
						 | 
					4c63ee23cd | ||
| 
						 | 
					c75d9e3de4 | ||
| 
						 | 
					df222ded12 | ||
| 
						 | 
					7dc0f81d3f | ||
| 
						 | 
					23793e834d | ||
| 
						 | 
					f4f3c6ad5a | ||
| 
						 | 
					775794e0e4 | ||
| 
						 | 
					03268ce4d8 | ||
| 
						 | 
					212d15fdd0 | ||
| 
						 | 
					065f015f7b | ||
| 
						 | 
					b5ba05dd83 | ||
| 
						 | 
					e4fda6cacf | ||
| 
						 | 
					accb526cd6 | ||
| 
						 | 
					2f0d94a46b | ||
| 
						 | 
					8dc24403d8 | ||
| 
						 | 
					a8c70d84a9 | ||
| 
						 | 
					10d7a64f88 | ||
| 
						 | 
					d51bbb4a81 | ||
| 
						 | 
					848f794149 | ||
| 
						 | 
					7f1b44befe | ||
| 
						 | 
					9ddd5a0566 | ||
| 
						 | 
					a3b664763e | ||
| 
						 | 
					fd47bc1dc3 | ||
| 
						 | 
					dfaafe3adb | ||
| 
						 | 
					b4dc4d34eb | ||
| 
						 | 
					3ae8ec1af6 | ||
| 
						 | 
					5c34666334 | ||
| 
						 | 
					212605a7e3 | ||
| 
						 | 
					4ddfa9af8d | ||
| 
						 | 
					36a0c7b8a3 | ||
| 
						 | 
					9e1e0a7252 | ||
| 
						 | 
					027e5adf67 | ||
| 
						 | 
					e986088bec | ||
| 
						 | 
					63ffd473d5 | ||
| 
						 | 
					9e5d92dc58 | ||
| 
						 | 
					8ac9141a29 | ||
| 
						 | 
					313c942350 | ||
| 
						 | 
					26c3edd023 | ||
| 
						 | 
					9a5a3d4ce4 | ||
| 
						 | 
					6e79b9a7a2 | ||
| 
						 | 
					b32d82e6c1 | ||
| 
						 | 
					a3585685df | ||
| 
						 | 
					f379865e2c | ||
| 
						 | 
					4eb4c31438 | ||
| 
						 | 
					84a7afcd94 | ||
| 
						 | 
					fa48ace39b | ||
| 
						 | 
					1b869d9305 | ||
| 
						 | 
					93bc2f5870 | ||
| 
						 | 
					37c0cfe1e9 | ||
| 
						 | 
					fc27441561 | ||
| 
						 | 
					79cfbac11f | ||
| 
						 | 
					df62736ff6 | ||
| 
						 | 
					6a464b3e5f | ||
| 
						 | 
					57fcda80df | ||
| 
						 | 
					db39fbc419 | ||
| 
						 | 
					3dabe47c78 | ||
| 
						 | 
					affc194cde | ||
| 
						 | 
					03fa580a55 | ||
| 
						 | 
					d0dce654bf | ||
| 
						 | 
					169323e238 | ||
| 
						 | 
					71df415b14 | ||
| 
						 | 
					6bb01bc564 | ||
| 
						 | 
					07c6fe5975 | ||
| 
						 | 
					5964181cb2 | ||
| 
						 | 
					4b8288a2b2 | ||
| 
						 | 
					88b1c1c6a5 | ||
| 
						 | 
					1234deabfa | ||
| 
						 | 
					ebaeb5a0d5 | ||
| 
						 | 
					45306bbb6c | ||
| 
						 | 
					18e2403b01 | ||
| 
						 | 
					e578c5f3ad | ||
| 
						 | 
					61245e3d7e | ||
| 
						 | 
					7804182d0d | ||
| 
						 | 
					f2195154f6 | ||
| 
						 | 
					35f77f45a2 | ||
| 
						 | 
					992c3a5d3a | ||
| 
						 | 
					d51d7b6797 | ||
| 
						 | 
					23ac2efd89 | ||
| 
						 | 
					daeffb2dc6 | ||
| 
						 | 
					db58ca6c1d | ||
| 
						 | 
					2ff292cbfa | ||
| 
						 | 
					5a81393863 | ||
| 
						 | 
					116a73d398 | ||
| 
						 | 
					cf0c057164 | ||
| 
						 | 
					fe5a4f4447 | ||
| 
						 | 
					c1b74201e4 | ||
| 
						 | 
					27828d9ca8 | ||
| 
						 | 
					2bd799fac6 | ||
| 
						 | 
					9275f2d753 | ||
| 
						 | 
					7455978ee5 | ||
| 
						 | 
					7c0acc7b77 | ||
| 
						 | 
					f32dd69acf | ||
| 
						 | 
					80b8f956a9 | ||
| 
						 | 
					caf50b6e6c | ||
| 
						 | 
					b590d0857c | ||
| 
						 | 
					982019307c | ||
| 
						 | 
					09aec7b22e | ||
| 
						 | 
					f9a047aad4 | ||
| 
						 | 
					85704570f3 | ||
| 
						 | 
					53dcae9e9c | ||
| 
						 | 
					04e1ab63bb | ||
| 
						 | 
					ed9aae531e | ||
| 
						 | 
					6ab6b3dbca | ||
| 
						 | 
					7180ed9a60 | ||
| 
						 | 
					0a5522d28c | ||
| 
						 | 
					c7bc93b32b | ||
| 
						 | 
					d30351e7b0 | ||
| 
						 | 
					886ffc0af8 | ||
| 
						 | 
					4fdd997108 | ||
| 
						 | 
					236736deea | ||
| 
						 | 
					2b317f60c8 | ||
| 
						 | 
					3ec67f9f47 | ||
| 
						 | 
					6435e7a30e | ||
| 
						 | 
					078305f5ac | ||
| 
						 | 
					801b62543a | ||
| 
						 | 
					877668b629 | ||
| 
						 | 
					f652f73260 | ||
| 
						 | 
					b2965e1deb | ||
| 
						 | 
					2214689920 | ||
| 
						 | 
					9326ff9d08 | ||
| 
						 | 
					271f58d9cf | ||
| 
						 | 
					cac99e3908 | ||
| 
						 | 
					97a4a910e0 | ||
| 
						 | 
					19c7a84548 | ||
| 
						 | 
					571ce11e53 | ||
| 
						 | 
					d2cb984ced | ||
| 
						 | 
					7fc0d11931 | ||
| 
						 | 
					341a52a615 | ||
| 
						 | 
					d58b99d602 | ||
| 
						 | 
					f7a5f836db | ||
| 
						 | 
					d212df8b95 | ||
| 
						 | 
					f3f6dc57c3 | ||
| 
						 | 
					29b5cd9436 | ||
| 
						 | 
					f5209fc344 | ||
| 
						 | 
					c5168c2132 | ||
| 
						 | 
					318e0989a2 | ||
| 
						 | 
					d8b1781d7b | ||
| 
						 | 
					ed5aea0521 | ||
| 
						 | 
					e9f90a4d82 | ||
| 
						 | 
					f86b220c92 | ||
| 
						 | 
					b6bb1673d4 | ||
| 
						 | 
					93f1762e6c | ||
| 
						 | 
					2f410fc09f | ||
| 
						 | 
					7b6fe66f2a | ||
| 
						 | 
					c2fc0b4979 | ||
| 
						 | 
					0b758941a4 | ||
| 
						 | 
					492b55c893 | ||
| 
						 | 
					4060e367ad | ||
| 
						 | 
					c99cd31b6b | ||
| 
						 | 
					718782f5b1 | ||
| 
						 | 
					0c3fb5b2ce | ||
| 
						 | 
					4ec6b067e7 | ||
| 
						 | 
					1748dd6a3b | ||
| 
						 | 
					95332e50ed | ||
| 
						 | 
					56eb9d1430 | ||
| 
						 | 
					8496980cf9 | ||
| 
						 | 
					ffe32694b0 | ||
| 
						 | 
					3d5b21154b | ||
| 
						 | 
					4b9697e336 | ||
| 
						 | 
					b0e9a542ba | ||
| 
						 | 
					8b67536c23 | ||
| 
						 | 
					cd49c12181 | ||
| 
						 | 
					a6b14c7910 | ||
| 
						 | 
					e275abdb9c | ||
| 
						 | 
					09a90665d5 | ||
| 
						 | 
					6649fbdfd0 | ||
| 
						 | 
					64a0ffee7b | ||
| 
						 | 
					b529118f31 | ||
| 
						 | 
					39d7d9f13a | ||
| 
						 | 
					fcd55df969 | ||
| 
						 | 
					1e59948358 | ||
| 
						 | 
					1102ef6e6b | ||
| 
						 | 
					7ce2e8f4c4 | ||
| 
						 | 
					fd1c656bdd | ||
| 
						 | 
					82298a760a | ||
| 
						 | 
					b84bb72e07 | ||
| 
						 | 
					495b321d0a | ||
| 
						 | 
					8a38cdc1d7 | ||
| 
						 | 
					e210db7bd9 | ||
| 
						 | 
					f64763b74b | ||
| 
						 | 
					e033bef544 | ||
| 
						 | 
					d88cc575e5 | ||
| 
						 | 
					e3f499be0c | ||
| 
						 | 
					87325fad74 | ||
| 
						 | 
					86220573b6 | ||
| 
						 | 
					65ed6b02a4 | ||
| 
						 | 
					98093a1f31 | ||
| 
						 | 
					00990dc195 | ||
| 
						 | 
					122aa94c9f | ||
| 
						 | 
					3da5284a07 | ||
| 
						 | 
					cd920364f8 | ||
| 
						 | 
					e2e8a45104 | ||
| 
						 | 
					fb5fc13f72 | ||
| 
						 | 
					edb92f7bfb | ||
| 
						 | 
					1980f43b9f | ||
| 
						 | 
					c3c3dd5154 | ||
| 
						 | 
					356155fd30 | ||
| 
						 | 
					6f75ef8f0a | ||
| 
						 | 
					ca865a80dc | ||
| 
						 | 
					8f759d1c3e | ||
| 
						 | 
					44787637f2 | ||
| 
						 | 
					cf1c8e8f2a | ||
| 
						 | 
					d948be2372 | ||
| 
						 | 
					cc28aef625 | ||
| 
						 | 
					036358de7c | ||
| 
						 | 
					0958b9ee12 | ||
| 
						 | 
					aff1d7ecd6 | ||
| 
						 | 
					42fdbd9bb8 | ||
| 
						 | 
					034c82e514 | ||
| 
						 | 
					14ff46b5cd | ||
| 
						 | 
					c9099ca0a5 | ||
| 
						 | 
					58b144b345 | ||
| 
						 | 
					af21c57e77 | ||
| 
						 | 
					624e4dbaaf | ||
| 
						 | 
					9bbb4f396b | ||
| 
						 | 
					1287e39cc6 | ||
| 
						 | 
					1ef2aa35e9 | ||
| 
						 | 
					b2c1644d69 | ||
| 
						 | 
					5629f842da | ||
| 
						 | 
					f900283b09 | ||
| 
						 | 
					b667eff6bd | ||
| 
						 | 
					54fdf40f5a | ||
| 
						 | 
					690542145d | ||
| 
						 | 
					94c4cf0624 | ||
| 
						 | 
					3da717d9fc | ||
| 
						 | 
					0902efc719 | ||
| 
						 | 
					d7e2ee63d8 | ||
| 
						 | 
					7deb36ee1f | ||
| 
						 | 
					bfe4e88246 | ||
| 
						 | 
					9ab45c3969 | ||
| 
						 | 
					fec80c6c51 | ||
| 
						 | 
					a6b7432358 | ||
| 
						 | 
					3486954e07 | ||
| 
						 | 
					150fc84b9b | ||
| 
						 | 
					b023a00445 | ||
| 
						 | 
					d0e296adf8 | ||
| 
						 | 
					aa40015e9b | ||
| 
						 | 
					141ce2c99a | ||
| 
						 | 
					4a95dcb6e9 | ||
| 
						 | 
					1610675c8f | ||
| 
						 | 
					724c814bfe | ||
| 
						 | 
					764c0cb865 | ||
| 
						 | 
					8a4b8a84d6 | ||
| 
						 | 
					8ec6acc55a | ||
| 
						 | 
					b6a022b0ef | ||
| 
						 | 
					716899c030 | ||
| 
						 | 
					d9e407fd2b | ||
| 
						 | 
					deb140de73 | ||
| 
						 | 
					3c1e5e7978 | ||
| 
						 | 
					4a8e85c28a | ||
| 
						 | 
					8498cadae8 | ||
| 
						 | 
					8c83fe23a1 | ||
| 
						 | 
					a8c65e3d27 | ||
| 
						 | 
					324d30bef9 | ||
| 
						 | 
					46cb48023e | ||
| 
						 | 
					1c24ca58c7 | ||
| 
						 | 
					9193a9a0e0 | ||
| 
						 | 
					957244ba2e | ||
| 
						 | 
					ac599aa47c | ||
| 
						 | 
					67a90ffb76 | ||
| 
						 | 
					feaa6f9bf0 | ||
| 
						 | 
					753bf3b924 | ||
| 
						 | 
					b3219f57c8 | ||
| 
						 | 
					a17df037af | ||
| 
						 | 
					dfc36e5210 | ||
| 
						 | 
					c359b92ddc | ||
| 
						 | 
					e1d6131f13 | ||
| 
						 | 
					6a0bda00f5 | ||
| 
						 | 
					f85ec95877 | ||
| 
						 | 
					a024980c03 | ||
| 
						 | 
					fd9e94e078 | ||
| 
						 | 
					f6a6c51d15 | ||
| 
						 | 
					966db1e4be | ||
| 
						 | 
					b8bbc37b8e | ||
| 
						 | 
					40cbabc330 | ||
| 
						 | 
					04a4e1b39a | ||
| 
						 | 
					99f3160aa2 | ||
| 
						 | 
					8cb72d8452 | ||
| 
						 | 
					c9eb9f3eda | ||
| 
						 | 
					64c3dcd732 | ||
| 
						 | 
					d49ececcc5 | ||
| 
						 | 
					90e1fadb1e | ||
| 
						 | 
					071391ddff | ||
| 
						 | 
					d70d46b4d5 | ||
| 
						 | 
					3ef596b215 | ||
| 
						 | 
					35c5518668 | ||
| 
						 | 
					8b513537b7 | ||
| 
						 | 
					b27f394995 | ||
| 
						 | 
					3f9f556e1c | ||
| 
						 | 
					1772d5c4b6 | ||
| 
						 | 
					715d1dc02f | ||
| 
						 | 
					6737f016f5 | ||
| 
						 | 
					f2d2622172 | ||
| 
						 | 
					72d6f97024 | ||
| 
						 | 
					a0f0b4ff9e | ||
| 
						 | 
					c27ef6ffbf | ||
| 
						 | 
					f5499ff699 | ||
| 
						 | 
					c4334d4e5f | ||
| 
						 | 
					51e8f0440d | ||
| 
						 | 
					5ec0311f84 | ||
| 
						 | 
					556d563ba0 | ||
| 
						 | 
					6a083b24c4 | ||
| 
						 | 
					825929fdc8 | ||
| 
						 | 
					941a03ed6c | ||
| 
						 | 
					cf63619182 | ||
| 
						 | 
					5c04d3c5ea | ||
| 
						 | 
					46a47db2d8 | ||
| 
						 | 
					21ef9a4567 | ||
| 
						 | 
					6f0846b2af | ||
| 
						 | 
					ecd78b3bdd | ||
| 
						 | 
					d8afd1af88 | ||
| 
						 | 
					7c1bc1f1a1 | ||
| 
						 | 
					763fc89b29 | ||
| 
						 | 
					47b33f2b17 | ||
| 
						 | 
					9f0e16b045 | ||
| 
						 | 
					2efedb1736 | ||
| 
						 | 
					044116c14c | ||
| 
						 | 
					b4bf11d648 | ||
| 
						 | 
					6cc0a5a1a4 | ||
| 
						 | 
					8f14de5108 | ||
| 
						 | 
					8f6e5d73a2 | ||
| 
						 | 
					ab9f5382b2 | ||
| 
						 | 
					fd441d9303 | ||
| 
						 | 
					e31bec3aff | ||
| 
						 | 
					2a1c05a028 | ||
| 
						 | 
					421bf33c0e | ||
| 
						 | 
					9c7bacc65e | ||
| 
						 | 
					3935c725c9 | ||
| 
						 | 
					908ee0060f | ||
| 
						 | 
					82e6fd7bb5 | ||
| 
						 | 
					6b98b14179 | ||
| 
						 | 
					1ecefd88f7 | ||
| 
						 | 
					2e9e20ce7c | ||
| 
						 | 
					fb60fbb217 | ||
| 
						 | 
					4199e17da0 | ||
| 
						 | 
					86b8bfcb1f | ||
| 
						 | 
					dfd089132d | ||
| 
						 | 
					3a10f58b28 | ||
| 
						 | 
					9d55adbaf2 | ||
| 
						 | 
					00be2be24f | ||
| 
						 | 
					5b126c7e52 | ||
| 
						 | 
					1943f3b53f | ||
| 
						 | 
					4a0bef9afb | ||
| 
						 | 
					dfd2a53129 | ||
| 
						 | 
					aa4e855012 | ||
| 
						 | 
					d6089e6309 | ||
| 
						 | 
					038e6df8f0 | ||
| 
						 | 
					2fd68bcac3 | ||
| 
						 | 
					e468fecf12 | ||
| 
						 | 
					fc31d8e5d1 | ||
| 
						 | 
					115f357a07 | ||
| 
						 | 
					ac04a1cac8 | ||
| 
						 | 
					87a286ef07 | ||
| 
						 | 
					622d8a4edb | ||
| 
						 | 
					b44086f0dc | ||
| 
						 | 
					0236e13187 | ||
| 
						 | 
					a3d4a7253f | ||
| 
						 | 
					e079f1b31a | ||
| 
						 | 
					9a78a72eb3 | ||
| 
						 | 
					862c2e8810 | ||
| 
						 | 
					12cad4c418 | ||
| 
						 | 
					89b9d3a7f7 | ||
| 
						 | 
					57831d4880 | ||
| 
						 | 
					052004d70e | ||
| 
						 | 
					a765237441 | ||
| 
						 | 
					ac470a6d07 | ||
| 
						 | 
					7237d33be3 | ||
| 
						 | 
					1610b480af | ||
| 
						 | 
					287fa0a39c | ||
| 
						 | 
					afa1a4303b | ||
| 
						 | 
					28cedb1493 | ||
| 
						 | 
					a280e25ee7 | ||
| 
						 | 
					8464ca8931 | ||
| 
						 | 
					44340f277d | ||
| 
						 | 
					74bf99f9ea | ||
| 
						 | 
					9caf820758 | ||
| 
						 | 
					26c2598f56 | ||
| 
						 | 
					6d9abf261c | ||
| 
						 | 
					b16d0185dd | ||
| 
						 | 
					ca51c2e93d | ||
| 
						 | 
					da254975cd | ||
| 
						 | 
					86bae6be3a | ||
| 
						 | 
					fc8c7ef18d | ||
| 
						 | 
					f654629c6a | ||
| 
						 | 
					d4a87c561a | ||
| 
						 | 
					d8872d48b3 | ||
| 
						 | 
					ed16c2c18d | ||
| 
						 | 
					0478a6ce3b | ||
| 
						 | 
					68b60e82ba | ||
| 
						 | 
					8edc0989e2 | ||
| 
						 | 
					fbf3551bbe | ||
| 
						 | 
					238d3122c4 | ||
| 
						 | 
					ee22fba448 | ||
| 
						 | 
					f8a2a28bff | ||
| 
						 | 
					b3cfaf1420 | ||
| 
						 | 
					2e9f701bb7 | ||
| 
						 | 
					9aabc4ad6a | ||
| 
						 | 
					5dc731bc77 | ||
| 
						 | 
					32d05c9855 | ||
| 
						 | 
					5a0d0c0b75 | ||
| 
						 | 
					17d4a8fb26 | ||
| 
						 | 
					348c1a7d5f | ||
| 
						 | 
					49151dabf5 | ||
| 
						 | 
					eb7c7cdcb6 | ||
| 
						 | 
					ec95292209 | ||
| 
						 | 
					4b84fb328c | ||
| 
						 | 
					47d27c1f41 | ||
| 
						 | 
					5267ad46da | ||
| 
						 | 
					94bc880b7f | ||
| 
						 | 
					bab3e0bc9b | ||
| 
						 | 
					b3a324b6f5 | ||
| 
						 | 
					a1117cd4ee | ||
| 
						 | 
					6ece818d69 | ||
| 
						 | 
					5df09d5e2a | ||
| 
						 | 
					33450ce429 | ||
| 
						 | 
					e2f0206d88 | ||
| 
						 | 
					3767b2c7f9 | ||
| 
						 | 
					1779f1f3da | ||
| 
						 | 
					b9d1dca65d | ||
| 
						 | 
					8e4d26163a | ||
| 
						 | 
					53c1176cbf | ||
| 
						 | 
					46d3e7884b | ||
| 
						 | 
					a0290b0c1b | ||
| 
						 | 
					b4ae706914 | ||
| 
						 | 
					476bdac717 | ||
| 
						 | 
					831627268d | ||
| 
						 | 
					9b97dca601 | ||
| 
						 | 
					4ea8c0802a | ||
| 
						 | 
					9203870df5 | ||
| 
						 | 
					e8088d6e38 | ||
| 
						 | 
					59d9bcdd27 | ||
| 
						 | 
					9d1b13ba73 | ||
| 
						 | 
					dd1030139b | ||
| 
						 | 
					30ca2117bb | ||
| 
						 | 
					89024a8dc8 | ||
| 
						 | 
					728c38396a | ||
| 
						 | 
					d61cb98ac7 | ||
| 
						 | 
					a7ceb61e27 | ||
| 
						 | 
					74b915a790 | ||
| 
						 | 
					01ea690421 | ||
| 
						 | 
					17cc9284a0 | ||
| 
						 | 
					498d0f0b8b | ||
| 
						 | 
					89049e1a22 | ||
| 
						 | 
					5e7254e8dc | ||
| 
						 | 
					f8c2732fdc | ||
| 
						 | 
					fec36eb298 | ||
| 
						 | 
					2299a4156d | ||
| 
						 | 
					32b82b9cb3 | ||
| 
						 | 
					ba6039fc8b | ||
| 
						 | 
					6885812d21 | ||
| 
						 | 
					844025ec14 | ||
| 
						 | 
					94bc91c554 | ||
| 
						 | 
					044c16da4c | ||
| 
						 | 
					cd4784c54a | ||
| 
						 | 
					814aaa4a69 | ||
| 
						 | 
					e3b3a4fefa | ||
| 
						 | 
					3fcbb3010d | ||
| 
						 | 
					7573a19dc9 | ||
| 
						 | 
					3628d68d9a | ||
| 
						 | 
					23872086fa | ||
| 
						 | 
					bb349a03da | ||
| 
						 | 
					82be426f78 | ||
| 
						 | 
					9d2a633f5e | ||
| 
						 | 
					1149d45589 | ||
| 
						 | 
					9d7e19cebf | ||
| 
						 | 
					b3023543d6 | ||
| 
						 | 
					c229d2c3ce | ||
| 
						 | 
					47ea383ddd | ||
| 
						 | 
					f2a35f1114 | ||
| 
						 | 
					147fc9a35a | ||
| 
						 | 
					93a03f8fe4 | ||
| 
						 | 
					230e3823a9 | ||
| 
						 | 
					b14a0f24ae | ||
| 
						 | 
					5295802720 | ||
| 
						 | 
					fadd7f6eb4 | ||
| 
						 | 
					011b76e4e7 | ||
| 
						 | 
					f68cd2c5c0 | ||
| 
						 | 
					6ac9789a1c | ||
| 
						 | 
					2b0153807c | ||
| 
						 | 
					34ab37f31e | ||
| 
						 | 
					71af2628eb | ||
| 
						 | 
					15f028abfb | ||
| 
						 | 
					9bdd37bb63 | ||
| 
						 | 
					d726c71141 | ||
| 
						 | 
					1caa61f4c0 | ||
| 
						 | 
					f3e3f08377 | ||
| 
						 | 
					2ec8b7a804 | ||
| 
						 | 
					9f7d137b05 | ||
| 
						 | 
					7218f13783 | ||
| 
						 | 
					fa31e7802c | ||
| 
						 | 
					9b3b4494ba | ||
| 
						 | 
					785d3748e1 | ||
| 
						 | 
					5e0657ce55 | ||
| 
						 | 
					700b06f9c5 | ||
| 
						 | 
					b58bbf8eb4 | ||
| 
						 | 
					2d1f522aaf | ||
| 
						 | 
					0b2863dfab | ||
| 
						 | 
					70907ead8a | ||
| 
						 | 
					6dc4844c12 | ||
| 
						 | 
					14bc1b6aac | ||
| 
						 | 
					183ad2a34b | ||
| 
						 | 
					d9758be3ae | ||
| 
						 | 
					6b1b530443 | ||
| 
						 | 
					1c20137b0e | ||
| 
						 | 
					c4a6c933f8 | ||
| 
						 | 
					31d9444264 | ||
| 
						 | 
					8cb204e22e | ||
| 
						 | 
					97aa72ec5b | ||
| 
						 | 
					a68341eae6 | ||
| 
						 | 
					aa08183439 | ||
| 
						 | 
					7a5596b909 | ||
| 
						 | 
					b9ffd50992 | ||
| 
						 | 
					14f2a8f370 | ||
| 
						 | 
					e7b16bfbc0 | ||
| 
						 | 
					a16725ac17 | ||
| 
						 | 
					2803a91673 | ||
| 
						 | 
					cf2fce7666 | ||
| 
						 | 
					1609abd166 | ||
| 
						 | 
					88c74ae18d | ||
| 
						 | 
					78e2b41e0c | ||
| 
						 | 
					501f8b028b | ||
| 
						 | 
					54401162bd | ||
| 
						 | 
					7fde9327a2 | ||
| 
						 | 
					bbbf59c74a | ||
| 
						 | 
					c4ad66f745 | ||
| 
						 | 
					69974d5651 | ||
| 
						 | 
					ce3b6a04c2 | ||
| 
						 | 
					37e2517dac | ||
| 
						 | 
					d65ddead11 | ||
| 
						 | 
					34034be0e3 | ||
| 
						 | 
					d21481173e | ||
| 
						 | 
					fa6ebadc7b | ||
| 
						 | 
					a51fb24f36 | ||
| 
						 | 
					c359b30763 | ||
| 
						 | 
					95e3b156c0 | ||
| 
						 | 
					b972a0d081 | ||
| 
						 | 
					20749355da | ||
| 
						 | 
					dad122199a | ||
| 
						 | 
					9fb8fbcc65 | ||
| 
						 | 
					78e7ea72dc | ||
| 
						 | 
					4640060891 | ||
| 
						 | 
					6efe4fb734 | ||
| 
						 | 
					74986803db | ||
| 
						 | 
					9b0a705055 | ||
| 
						 | 
					163fc9e3a3 | ||
| 
						 | 
					24bf7950d8 | ||
| 
						 | 
					b6735bffe4 | ||
| 
						 | 
					1d8fd480ca | ||
| 
						 | 
					da2e2372aa | ||
| 
						 | 
					f3b972e573 | ||
| 
						 | 
					bf3bc3c7e9 | ||
| 
						 | 
					38664487a0 | ||
| 
						 | 
					de1111286c | ||
| 
						 | 
					d89a12aa05 | ||
| 
						 | 
					754acd7c26 | ||
| 
						 | 
					c3e2f3b714 | ||
| 
						 | 
					22ef3d3a46 | ||
| 
						 | 
					7f3516f44f | ||
| 
						 | 
					bfdb47a7ed | ||
| 
						 | 
					f55f04ab4f | ||
| 
						 | 
					01c9dbc1fd | ||
| 
						 | 
					0aa807df19 | ||
| 
						 | 
					48d44ece58 | ||
| 
						 | 
					e58cb2b0db | ||
| 
						 | 
					bffd9d9173 | ||
| 
						 | 
					8688842984 | ||
| 
						 | 
					cf29a8f2c8 | ||
| 
						 | 
					1e00c89988 | ||
| 
						 | 
					0eccb547b5 | ||
| 
						 | 
					4789a7f6a9 | ||
| 
						 | 
					0bf758afd4 | ||
| 
						 | 
					6612550c06 | ||
| 
						 | 
					d411159124 | ||
| 
						 | 
					cf635a5e6f | ||
| 
						 | 
					3a007e4f3d | ||
| 
						 | 
					9faab960f6 | ||
| 
						 | 
					5df8b1d183 | ||
| 
						 | 
					ef5f910f19 | ||
| 
						 | 
					fffbee80e8 | ||
| 
						 | 
					6b30e167e1 | ||
| 
						 | 
					8ec721259a | ||
| 
						 | 
					9d7ce207b6 | ||
| 
						 | 
					2d1f0c9f57 | ||
| 
						 | 
					d3131d2f55 | ||
| 
						 | 
					c10447df79 | ||
| 
						 | 
					212ae76d76 | ||
| 
						 | 
					cd48f7eff4 | ||
| 
						 | 
					3513c6801e | ||
| 
						 | 
					864529cbf6 | ||
| 
						 | 
					58c0d3e12d | ||
| 
						 | 
					a1493bfb4e | ||
| 
						 | 
					b3e856df1d | ||
| 
						 | 
					8ef2617eec | ||
| 
						 | 
					1da7d81122 | ||
| 
						 | 
					a103582346 | ||
| 
						 | 
					7b61d05e88 | ||
| 
						 | 
					6fc7c50f19 | ||
| 
						 | 
					9d728ec3c5 | ||
| 
						 | 
					9cd3358e4e | ||
| 
						 | 
					4cd94370e8 | ||
| 
						 | 
					52312dbd23 | ||
| 
						 | 
					b2e8a1eaa2 | ||
| 
						 | 
					506c17a093 | ||
| 
						 | 
					69642fba52 | ||
| 
						 | 
					7d647c981f | ||
| 
						 | 
					9aec3b714e | ||
| 
						 | 
					dd4648ed9a | ||
| 
						 | 
					1cd0beb231 | ||
| 
						 | 
					c96e4b7966 | ||
| 
						 | 
					b7aab3c102 | ||
| 
						 | 
					fcb1a657e3 | ||
| 
						 | 
					9b2cb1e1c3 | ||
| 
						 | 
					fb8b8d28da | ||
| 
						 | 
					ad80153bbb | ||
| 
						 | 
					9564b261d5 | ||
| 
						 | 
					1e2a662fa6 | ||
| 
						 | 
					51f7daaeaf | ||
| 
						 | 
					f742a7ec4e | ||
| 
						 | 
					e2c0d2a07b | ||
| 
						 | 
					d112dc41b2 | ||
| 
						 | 
					2322851ac4 | ||
| 
						 | 
					aa084ea09a | ||
| 
						 | 
					6520f9b7eb | ||
| 
						 | 
					fd8d0a1746 | ||
| 
						 | 
					af3ebacee6 | ||
| 
						 | 
					55d7014301 | ||
| 
						 | 
					b72d7fbeda | ||
| 
						 | 
					ee15c14049 | ||
| 
						 | 
					1756bdd033 | ||
| 
						 | 
					0cffaf8dc5 | ||
| 
						 | 
					55a93e7b47 | ||
| 
						 | 
					5dc5bfb797 | ||
| 
						 | 
					f101ee3c4f | ||
| 
						 | 
					6319f41b2c | ||
| 
						 | 
					6c718ada1b | ||
| 
						 | 
					67acc38a1f | ||
| 
						 | 
					dd1d8509f0 | ||
| 
						 | 
					79f342439a | ||
| 
						 | 
					13db64f0ec | ||
| 
						 | 
					908ce3bbd9 | ||
| 
						 | 
					df3313971d | ||
| 
						 | 
					b175132854 | ||
| 
						 | 
					4cb0655192 | ||
| 
						 | 
					8b191bd2f7 | ||
| 
						 | 
					f3106e3bbb | ||
| 
						 | 
					7fcfbc3729 | ||
| 
						 | 
					598468c2b7 | ||
| 
						 | 
					84681d3878 | ||
| 
						 | 
					c7b14cba4d | ||
| 
						 | 
					d508127452 | ||
| 
						 | 
					984c79e2d2 | ||
| 
						 | 
					6cb296f952 | ||
| 
						 | 
					db533fc166 | ||
| 
						 | 
					02b0e79ba3 | ||
| 
						 | 
					1b83dd0a8a | ||
| 
						 | 
					9b982b408d | ||
| 
						 | 
					9b03ab830d | ||
| 
						 | 
					264da6798c | ||
| 
						 | 
					f68b8afa8d | ||
| 
						 | 
					63f9063255 | ||
| 
						 | 
					6dad353e1c | ||
| 
						 | 
					5446d8d4a2 | ||
| 
						 | 
					ef7617d545 | ||
| 
						 | 
					0fbb560e90 | ||
| 
						 | 
					86b5c55855 | ||
| 
						 | 
					768decde93 | ||
| 
						 | 
					3cb4315193 | ||
| 
						 | 
					69b079c86e | ||
| 
						 | 
					9f3fc5eb9f | ||
| 
						 | 
					15e595837b | ||
| 
						 | 
					17e57bb28e | ||
| 
						 | 
					4d0c77b973 | ||
| 
						 | 
					f8b180ac44 | ||
| 
						 | 
					cd30368da9 | ||
| 
						 | 
					27ed57a648 | ||
| 
						 | 
					e38b527ac2 | ||
| 
						 | 
					113d9612db | ||
| 
						 | 
					6b3daec23f | ||
| 
						 | 
					e056a1d46d | ||
| 
						 | 
					57026f6262 | ||
| 
						 | 
					8ef77f50c3 | ||
| 
						 | 
					93e21515e5 | ||
| 
						 | 
					24caa3b97b | ||
| 
						 | 
					c93b36fe79 | ||
| 
						 | 
					0de9242a26 | ||
| 
						 | 
					53fb52c6c0 | ||
| 
						 | 
					afaa529ba6 | ||
| 
						 | 
					43824bd621 | ||
| 
						 | 
					3c97a4f5a1 | ||
| 
						 | 
					711bf190d4 | ||
| 
						 | 
					1049006cf9 | ||
| 
						 | 
					76603d108d | ||
| 
						 | 
					5bc3930230 | ||
| 
						 | 
					e5edd851b3 | ||
| 
						 | 
					dcad400758 | ||
| 
						 | 
					a1aaea9c55 | ||
| 
						 | 
					4d6b981a54 | ||
| 
						 | 
					a4e4286e04 | ||
| 
						 | 
					6dd7a6a171 | ||
| 
						 | 
					8e554a87b0 | ||
| 
						 | 
					f1b4c083a4 | ||
| 
						 | 
					90af4e3b77 | ||
| 
						 | 
					e8d76a513d | ||
| 
						 | 
					29e03b88c7 | ||
| 
						 | 
					ebbd870150 | ||
| 
						 | 
					5bf402710f | ||
| 
						 | 
					c0c54e5709 | ||
| 
						 | 
					3ba984d09e | ||
| 
						 | 
					f274683d46 | ||
| 
						 | 
					2053db4cfc | ||
| 
						 | 
					e20ce8e335 | ||
| 
						 | 
					9fd750511c | ||
| 
						 | 
					028957fcdc | ||
| 
						 | 
					a4c54cae60 | ||
| 
						 | 
					754303e7c7 | ||
| 
						 | 
					cc0eae7153 | ||
| 
						 | 
					066ca9e552 | ||
| 
						 | 
					7c04a90d77 | ||
| 
						 | 
					a8a65ac769 | ||
| 
						 | 
					aec3c5d6cc | ||
| 
						 | 
					a22141c2eb | ||
| 
						 | 
					99aa064319 | ||
| 
						 | 
					6aaf83f3c2 | ||
| 
						 | 
					133ce39a13 | ||
| 
						 | 
					8645214654 | ||
| 
						 | 
					eebc334e02 | ||
| 
						 | 
					038fa3b301 | ||
| 
						 | 
					9a8497299d | ||
| 
						 | 
					61ce3868b5 | ||
| 
						 | 
					844c2a26bc | ||
| 
						 | 
					a15c4d9c20 | ||
| 
						 | 
					ff9f0e60ac | ||
| 
						 | 
					2bf6111bf5 | ||
| 
						 | 
					ad10a11903 | ||
| 
						 | 
					c22153a4eb | ||
| 
						 | 
					5348d57057 | ||
| 
						 | 
					052524dabd | ||
| 
						 | 
					e33d05cfe5 | ||
| 
						 | 
					5529ece220 | ||
| 
						 | 
					e71094d4a8 | ||
| 
						 | 
					98aa023d70 | ||
| 
						 | 
					e1066434d0 | ||
| 
						 | 
					86ae4b2a75 | ||
| 
						 | 
					ed8099bf1e | ||
| 
						 | 
					524c9beee4 | ||
| 
						 | 
					99fb9dcf11 | ||
| 
						 | 
					1294817103 | ||
| 
						 | 
					9775660da7 | ||
| 
						 | 
					e7051353eb | ||
| 
						 | 
					bd19e97cf8 | ||
| 
						 | 
					8b821ac0c9 | ||
| 
						 | 
					43e5dc2292 | ||
| 
						 | 
					08fa22749a | ||
| 
						 | 
					c197962851 | ||
| 
						 | 
					44a51273be | ||
| 
						 | 
					e3b3ae97bc | ||
| 
						 | 
					410a22dc63 | ||
| 
						 | 
					069766d581 | ||
| 
						 | 
					f22e36e52f | ||
| 
						 | 
					bc1794fb4a | ||
| 
						 | 
					aacd26c7db | ||
| 
						 | 
					ff166f7b4c | ||
| 
						 | 
					bf1b5c3951 | ||
| 
						 | 
					22baebaf8c | ||
| 
						 | 
					e756506c18 | ||
| 
						 | 
					fd67f980a5 | ||
| 
						 | 
					e2da3406d2 | ||
| 
						 | 
					05b6d989b6 | ||
| 
						 | 
					1d6ee64e1d | ||
| 
						 | 
					bfefb99192 | ||
| 
						 | 
					47ae874e4d | ||
| 
						 | 
					d74f636558 | ||
| 
						 | 
					b8f0822214 | ||
| 
						 | 
					0869455612 | ||
| 
						 | 
					bca74241e6 | ||
| 
						 | 
					9d5801fb5f | ||
| 
						 | 
					462a88ae82 | ||
| 
						 | 
					887bec019a | ||
| 
						 | 
					d0463b2089 | ||
| 
						 | 
					deea4320ad | ||
| 
						 | 
					bb26c03141 | ||
| 
						 | 
					a5517a1a51 | ||
| 
						 | 
					4511aa4d21 | ||
| 
						 | 
					b25a0545f5 | ||
| 
						 | 
					1a97bd55c7 | ||
| 
						 | 
					bf711f2ad7 | ||
| 
						 | 
					e1b065c74a | ||
| 
						 | 
					eacc911cdc | ||
| 
						 | 
					91a5f4af9a | ||
| 
						 | 
					f6cdda9029 | ||
| 
						 | 
					981a3629b6 | ||
| 
						 | 
					3554872d9a | ||
| 
						 | 
					118e8f541c | ||
| 
						 | 
					3fa55f9022 | ||
| 
						 | 
					1c73c6cc70 | ||
| 
						 | 
					148c32a383 | ||
| 
						 | 
					ca08062bf3 | ||
| 
						 | 
					ecbab75a25 | ||
| 
						 | 
					15d25df245 | ||
| 
						 | 
					b2f7bdca34 | ||
| 
						 | 
					32bcdb8982 | ||
| 
						 | 
					ba3e7e7974 | ||
| 
						 | 
					fae82a30e7 | ||
| 
						 | 
					5ce1a55a4f | ||
| 
						 | 
					1132816902 | ||
| 
						 | 
					a4c1f1ac47 | ||
| 
						 | 
					f619e9df24 | ||
| 
						 | 
					43631a3271 | ||
| 
						 | 
					04855de495 | ||
| 
						 | 
					6206ceb49b | ||
| 
						 | 
					628f3ba05f | ||
| 
						 | 
					94f534f36f | ||
| 
						 | 
					2f11e7c087 | ||
| 
						 | 
					0739ced407 | ||
| 
						 | 
					8fb34465cd | ||
| 
						 | 
					04fe9fadb1 | ||
| 
						 | 
					998c95da51 | ||
| 
						 | 
					3bc771a0d9 | ||
| 
						 | 
					bdc25adb41 | ||
| 
						 | 
					7ec29c2eea | ||
| 
						 | 
					b9995e7f70 | ||
| 
						 | 
					31d9d2efcd | ||
| 
						 | 
					86f42d56f2 | ||
| 
						 | 
					f05bf0a6f6 | ||
| 
						 | 
					a06a2989d2 | ||
| 
						 | 
					0cf6614186 | ||
| 
						 | 
					ddc16cc2be | ||
| 
						 | 
					616f0b3e73 | ||
| 
						 | 
					bd1056b13f | ||
| 
						 | 
					5a7b9abe33 | ||
| 
						 | 
					cc86923fd5 | ||
| 
						 | 
					916d764477 | ||
| 
						 | 
					4a21a8fdae | ||
| 
						 | 
					49df5bd590 | ||
| 
						 | 
					9eaf492d5b | ||
| 
						 | 
					a80502f7db | ||
| 
						 | 
					eade013138 | ||
| 
						 | 
					912254751a | ||
| 
						 | 
					c3c6f7f7ae | ||
| 
						 | 
					5cf58d9446 | ||
| 
						 | 
					3ba598633c | ||
| 
						 | 
					406530ca69 | ||
| 
						 | 
					f8b963df6d | ||
| 
						 | 
					d17000975f | ||
| 
						 | 
					64a8ba6212 | ||
| 
						 | 
					350ddd2af1 | ||
| 
						 | 
					c03abdac42 | ||
| 
						 | 
					bb3cc2c821 | ||
| 
						 | 
					0b814eff83 | ||
| 
						 | 
					a91ac91977 | ||
| 
						 | 
					422d70d928 | ||
| 
						 | 
					a4cb859472 | ||
| 
						 | 
					19137b79bc | ||
| 
						 | 
					bd1e311674 | ||
| 
						 | 
					c127db335a | ||
| 
						 | 
					f5ed1604aa | ||
| 
						 | 
					771cc9e583 | ||
| 
						 | 
					7ad1e246a0 | ||
| 
						 | 
					5c638251f8 | ||
| 
						 | 
					0d189dc78e | ||
| 
						 | 
					a9d7253143 | ||
| 
						 | 
					199f29e63c | ||
| 
						 | 
					c82efceae0 | ||
| 
						 | 
					cad461b121 | ||
| 
						 | 
					5af68ac545 | ||
| 
						 | 
					c0ce70ccc7 | ||
| 
						 | 
					753c518d33 | ||
| 
						 | 
					b9ca6694f7 | ||
| 
						 | 
					0c116251b1 | ||
| 
						 | 
					e9def2cdc5 | ||
| 
						 | 
					8ca525dc7a | ||
| 
						 | 
					281fe6927a | ||
| 
						 | 
					35471a41c8 | ||
| 
						 | 
					bda3098a31 | ||
| 
						 | 
					1e05eb1d60 | ||
| 
						 | 
					6369b902bf | ||
| 
						 | 
					aea794e522 | ||
| 
						 | 
					7c3dfb7bae | ||
| 
						 | 
					75057f9a91 | ||
| 
						 | 
					7026bd926a | ||
| 
						 | 
					ae19a0dc5f | ||
| 
						 | 
					7d9a2132cb | ||
| 
						 | 
					20f2f61349 | ||
| 
						 | 
					4169431f2c | ||
| 
						 | 
					45798f993d | ||
| 
						 | 
					ae0d68c27e | ||
| 
						 | 
					070b191ffe | ||
| 
						 | 
					778e88cb56 | ||
| 
						 | 
					d1fcfa05db | ||
| 
						 | 
					f4754dc5fc | ||
| 
						 | 
					0f885e73c3 | ||
| 
						 | 
					75acd4c1aa | ||
| 
						 | 
					3ef0621eb0 | ||
| 
						 | 
					b1db9ead0d | ||
| 
						 | 
					9cc6000d89 | ||
| 
						 | 
					42515899d9 | ||
| 
						 | 
					1d096eeb17 | ||
| 
						 | 
					bc5b8f0ff9 | ||
| 
						 | 
					bf412fd309 | ||
| 
						 | 
					6e1f4246b8 | ||
| 
						 | 
					ed88619623 | ||
| 
						 | 
					37b8922348 | ||
| 
						 | 
					4a23a0064b | ||
| 
						 | 
					e012b1e5d1 | ||
| 
						 | 
					2bc84ec908 | ||
| 
						 | 
					8efe26ed57 | ||
| 
						 | 
					24d7dc9bf9 | ||
| 
						 | 
					4b7139d9ae | ||
| 
						 | 
					1f35624121 | ||
| 
						 | 
					860c5b8f20 | ||
| 
						 | 
					a454c39ca0 | ||
| 
						 | 
					6938910f08 | ||
| 
						 | 
					2c63dde6c5 | ||
| 
						 | 
					e69d20a209 | ||
| 
						 | 
					0b731edd21 | ||
| 
						 | 
					efdd61595e | ||
| 
						 | 
					d676f88fed | ||
| 
						 | 
					07f2cf7ad0 | ||
| 
						 | 
					bcb520ed3b | ||
| 
						 | 
					943a2707d2 | ||
| 
						 | 
					93cee18300 | ||
| 
						 | 
					1442337e3c | ||
| 
						 | 
					cae4655785 | ||
| 
						 | 
					28c12606a4 | ||
| 
						 | 
					54df355014 | ||
| 
						 | 
					8dc8682078 | ||
| 
						 | 
					36e9c6ac4d | ||
| 
						 | 
					10ea9bf1e3 | ||
| 
						 | 
					cf50299b14 | ||
| 
						 | 
					2c12be62c4 | ||
| 
						 | 
					4636a75b5e | ||
| 
						 | 
					03756e364a | ||
| 
						 | 
					ce1715c79e | ||
| 
						 | 
					a62ab3c649 | ||
| 
						 | 
					bfb7b988f4 | ||
| 
						 | 
					84f41262f5 | ||
| 
						 | 
					dda40e29f4 | ||
| 
						 | 
					7df868e22a | ||
| 
						 | 
					bf5e7aaa48 | ||
| 
						 | 
					d76e744eab | ||
| 
						 | 
					6f5699fe09 | ||
| 
						 | 
					f9d916925e | ||
| 
						 | 
					f9258878db | ||
| 
						 | 
					ef9e86b50d | ||
| 
						 | 
					b21931c667 | ||
| 
						 | 
					06de3f5e69 | ||
| 
						 | 
					261a8fd83b | ||
| 
						 | 
					6527074cde | ||
| 
						 | 
					fe0f078353 | ||
| 
						 | 
					f2485931d9 | ||
| 
						 | 
					4f8a0b7711 | ||
| 
						 | 
					2dde55050e | ||
| 
						 | 
					6aade62ce2 | ||
| 
						 | 
					45b88ebb2a | ||
| 
						 | 
					dc7159a450 | ||
| 
						 | 
					536ace8e10 | ||
| 
						 | 
					16b2a3e66e | ||
| 
						 | 
					6f135a0cce | ||
| 
						 | 
					cf220dd2eb | ||
| 
						 | 
					914f4fb862 | ||
| 
						 | 
					7bdb68eecf | ||
| 
						 | 
					3c510cfaf0 | ||
| 
						 | 
					401fa198c9 | ||
| 
						 | 
					600df4f63a | ||
| 
						 | 
					74eb42c111 | ||
| 
						 | 
					39f3afd52c | ||
| 
						 | 
					9876a1aeca | ||
| 
						 | 
					d898ffce23 | ||
| 
						 | 
					f1772f4625 | ||
| 
						 | 
					9da455a7ea | ||
| 
						 | 
					2bd6342309 | ||
| 
						 | 
					5e73577088 | ||
| 
						 | 
					5156a80738 | ||
| 
						 | 
					a9d605ed30 | ||
| 
						 | 
					7d1fae32cd | ||
| 
						 | 
					9f17b45bc4 | ||
| 
						 | 
					a64c9dae42 | ||
| 
						 | 
					5fbf4c394c | ||
| 
						 | 
					1e5153173c | ||
| 
						 | 
					011b52d07d | ||
| 
						 | 
					d033168d80 | ||
| 
						 | 
					fdca9e59de | ||
| 
						 | 
					549a2fd206 | ||
| 
						 | 
					a0cd939bfd | ||
| 
						 | 
					4f52679ec6 | ||
| 
						 | 
					b52e237044 | ||
| 
						 | 
					17b329bb63 | ||
| 
						 | 
					3a654ba199 | ||
| 
						 | 
					5ba3fc9321 | ||
| 
						 | 
					a46f08154e | ||
| 
						 | 
					0f6ed9c293 | ||
| 
						 | 
					295864d36b | ||
| 
						 | 
					be6d45e49f | ||
| 
						 | 
					22b6987249 | ||
| 
						 | 
					64647b0bb3 | ||
| 
						 | 
					a5a1f2e8ad | ||
| 
						 | 
					8bd39f33e0 | ||
| 
						 | 
					be9774943b | ||
| 
						 | 
					dd6e79922a | ||
| 
						 | 
					bf84269520 | ||
| 
						 | 
					943214c6a7 | ||
| 
						 | 
					ca792669fc | ||
| 
						 | 
					78d68a9949 | ||
| 
						 | 
					6b2db97347 | ||
| 
						 | 
					2bfb362832 | ||
| 
						 | 
					cb140e482f | ||
| 
						 | 
					e6b72ac1ff | ||
| 
						 | 
					8032e6d68d | ||
| 
						 | 
					c7e0a6f37f | ||
| 
						 | 
					1141cd2e6e | ||
| 
						 | 
					c9dd953817 | ||
| 
						 | 
					b7ffca031e | ||
| 
						 | 
					544bab0fe2 | ||
| 
						 | 
					fd2f441e02 | ||
| 
						 | 
					3b3ebda34b | ||
| 
						 | 
					87e3d663a2 | ||
| 
						 | 
					d0a1d910d4 | ||
| 
						 | 
					33b97082fa | ||
| 
						 | 
					d93f05f511 | ||
| 
						 | 
					3dc29197e1 | ||
| 
						 | 
					fbc0236748 | ||
| 
						 | 
					1304a3943a | ||
| 
						 | 
					8c0ba1aee2 | ||
| 
						 | 
					47154773f2 | ||
| 
						 | 
					deca9bf06d | ||
| 
						 | 
					6ab4c9be2e | ||
| 
						 | 
					3a519612a0 | ||
| 
						 | 
					d1ec26ae83 | ||
| 
						 | 
					9cb889c34f | ||
| 
						 | 
					77f27b7948 | ||
| 
						 | 
					dbee5bc515 | ||
| 
						 | 
					54a5332834 | ||
| 
						 | 
					8af164c99f | ||
| 
						 | 
					c7321fddfb | ||
| 
						 | 
					c5ca278253 | ||
| 
						 | 
					638fdd8c3e | ||
| 
						 | 
					3407b57a51 | ||
| 
						 | 
					836bf836d3 | ||
| 
						 | 
					6e52d14180 | ||
| 
						 | 
					cdf0311d27 | ||
| 
						 | 
					1096fe55e1 | ||
| 
						 | 
					d2d615c84a | ||
| 
						 | 
					9f26c8cecb | ||
| 
						 | 
					cd1f082c52 | ||
| 
						 | 
					5610f423d0 | ||
| 
						 | 
					b90dfb48ee | ||
| 
						 | 
					70e67a686b | ||
| 
						 | 
					d1c3d900ab | ||
| 
						 | 
					e8a4ad1001 | ||
| 
						 | 
					ccac85b079 | ||
| 
						 | 
					f92fefb6cd | ||
| 
						 | 
					b799213a14 | ||
| 
						 | 
					cc565cf57e | ||
| 
						 | 
					4a56e9c884 | ||
| 
						 | 
					d11d906fa2 | ||
| 
						 | 
					65c4a0c319 | ||
| 
						 | 
					55bcf78efe | ||
| 
						 | 
					f5a2ce52aa | ||
| 
						 | 
					0dcbfd746e | ||
| 
						 | 
					06e043d3f1 | ||
| 
						 | 
					986d34fb3e | ||
| 
						 | 
					5296ab1b09 | ||
| 
						 | 
					b5d3348d99 | ||
| 
						 | 
					1e77df381a | ||
| 
						 | 
					ec33281ff5 | ||
| 
						 | 
					ba67f13ad5 | ||
| 
						 | 
					1604ed91fb | ||
| 
						 | 
					74fcaab5e9 | ||
| 
						 | 
					adee435ea6 | ||
| 
						 | 
					ea59ab5176 | ||
| 
						 | 
					f78008c1b2 | ||
| 
						 | 
					f54db695af | ||
| 
						 | 
					f21f922160 | ||
| 
						 | 
					fd413c7b52 | ||
| 
						 | 
					3e2c5af4b5 | ||
| 
						 | 
					bdb49b1171 | ||
| 
						 | 
					5933b3d7eb | ||
| 
						 | 
					4c8d606fae | ||
| 
						 | 
					13c1d2fd2b | ||
| 
						 | 
					e35c807216 | ||
| 
						 | 
					5a2cc6f154 | ||
| 
						 | 
					88f8c43472 | ||
| 
						 | 
					ef3e8e6fac | ||
| 
						 | 
					1505372e20 | ||
| 
						 | 
					ad5093ce05 | ||
| 
						 | 
					b558d1afc6 | ||
| 
						 | 
					ddfd05b008 | ||
| 
						 | 
					d2ad01a9ff | ||
| 
						 | 
					64a17abfe2 | ||
| 
						 | 
					04b638aa06 | ||
| 
						 | 
					31e30906d0 | ||
| 
						 | 
					bc00be9065 | ||
| 
						 | 
					4a599e986f | ||
| 
						 | 
					f1ca03e378 | ||
| 
						 | 
					f3d5fc7a84 | ||
| 
						 | 
					3bfcdf9c41 | ||
| 
						 | 
					144200e315 | ||
| 
						 | 
					398e229c77 | ||
| 
						 | 
					6a61fe5776 | ||
| 
						 | 
					9835206452 | ||
| 
						 | 
					70b0580fb7 | ||
| 
						 | 
					23eb7732d7 | ||
| 
						 | 
					26e50cefea | ||
| 
						 | 
					588e907181 | ||
| 
						 | 
					eae7d6260f | ||
| 
						 | 
					175b4e7f92 | ||
| 
						 | 
					b050417ab1 | ||
| 
						 | 
					37b49400db | ||
| 
						 | 
					ebcb2e7837 | ||
| 
						 | 
					f1e7db6a88 | ||
| 
						 | 
					2ba0929458 | ||
| 
						 | 
					83fed42997 | ||
| 
						 | 
					2c4626709c | ||
| 
						 | 
					59fbadd9eb | ||
| 
						 | 
					adb860b464 | ||
| 
						 | 
					8d8790586d | ||
| 
						 | 
					61ca60c550 | ||
| 
						 | 
					d713d01600 | ||
| 
						 | 
					372ea0f845 | ||
| 
						 | 
					61888708f5 | ||
| 
						 | 
					c900459f73 | ||
| 
						 | 
					2c92f75c86 | ||
| 
						 | 
					3e1514239c | ||
| 
						 | 
					0707a1d49a | ||
| 
						 | 
					9521f19507 | ||
| 
						 | 
					bd69116df2 | ||
| 
						 | 
					6535986484 | ||
| 
						 | 
					e03db9c2d5 | ||
| 
						 | 
					038790370c | ||
| 
						 | 
					261bf0b298 | ||
| 
						 | 
					5a7bdcfe59 | ||
| 
						 | 
					4f3261b262 | ||
| 
						 | 
					48e6087b1b | ||
| 
						 | 
					28103c901d | ||
| 
						 | 
					368701610f | ||
| 
						 | 
					b589f48aa9 | ||
| 
						 | 
					74e32efa7d | ||
| 
						 | 
					dc555b2206 | ||
| 
						 | 
					859cf6930f | ||
| 
						 | 
					6f83fbd212 | ||
| 
						 | 
					a7be4780ba | ||
| 
						 | 
					b5e89d4440 | ||
| 
						 | 
					89bc98d26b | ||
| 
						 | 
					31a73273ac | ||
| 
						 | 
					2a820ea6e7 | ||
| 
						 | 
					6484d4f0bd | ||
| 
						 | 
					461e48be32 | ||
| 
						 | 
					ff60ffca3e | ||
| 
						 | 
					1bbf310c46 | ||
| 
						 | 
					8469f448b5 | ||
| 
						 | 
					e36abc3ac6 | ||
| 
						 | 
					605dd72354 | ||
| 
						 | 
					9770b65146 | ||
| 
						 | 
					57158890c3 | ||
| 
						 | 
					6f8f490fdd | ||
| 
						 | 
					415e9dc913 | ||
| 
						 | 
					1487762925 | ||
| 
						 | 
					c73a91a0f5 | ||
| 
						 | 
					5dced28088 | ||
| 
						 | 
					38f6956e71 | ||
| 
						 | 
					93da9dd1f4 | ||
| 
						 | 
					505c8cde81 | ||
| 
						 | 
					f7a6fa9873 | ||
| 
						 | 
					70a2d11b8c | ||
| 
						 | 
					2c077aca5a | ||
| 
						 | 
					efef6b098d | ||
| 
						 | 
					885f2a3226 | ||
| 
						 | 
					1f94ae7888 | ||
| 
						 | 
					05aa520669 | ||
| 
						 | 
					b0c32159e7 | ||
| 
						 | 
					4a182517da | ||
| 
						 | 
					107b584085 | ||
| 
						 | 
					6d8416f838 | ||
| 
						 | 
					876a0cb6f7 | ||
| 
						 | 
					4904612523 | ||
| 
						 | 
					3bd76b9156 | ||
| 
						 | 
					dd757e7d04 | ||
| 
						 | 
					71d06647d0 | ||
| 
						 | 
					0ae57589a9 | ||
| 
						 | 
					22a6819f7b | ||
| 
						 | 
					5e23ad2db1 | ||
| 
						 | 
					e009ea57b9 | ||
| 
						 | 
					63c93a42b5 | ||
| 
						 | 
					e6cc1625b5 | ||
| 
						 | 
					f65b0128e7 | ||
| 
						 | 
					507b7fee56 | ||
| 
						 | 
					925d28495a | ||
| 
						 | 
					19dd71eb05 | ||
| 
						 | 
					b3fd56c2c1 | ||
| 
						 | 
					d8b6ebf6cb | ||
| 
						 | 
					bdaac17886 | ||
| 
						 | 
					8cac51abbe | ||
| 
						 | 
					0113d4499b | ||
| 
						 | 
					7562ab3c1c | ||
| 
						 | 
					d3de07ecf3 | ||
| 
						 | 
					b146cd889f | ||
| 
						 | 
					bc0f184098 | ||
| 
						 | 
					1debde3046 | ||
| 
						 | 
					4d3fdbdd80 | ||
| 
						 | 
					90c201d438 | ||
| 
						 | 
					1e22abab80 | ||
| 
						 | 
					792c37735e | ||
| 
						 | 
					6d8ece530f | ||
| 
						 | 
					3499dfb285 | ||
| 
						 | 
					30c656ceda | ||
| 
						 | 
					a1c7f86ff3 | ||
| 
						 | 
					4952e41132 | ||
| 
						 | 
					e1142216ec | ||
| 
						 | 
					a4040fc1ee | ||
| 
						 | 
					f84572443f | ||
| 
						 | 
					fbf6bc1979 | ||
| 
						 | 
					3aeb2c1230 | ||
| 
						 | 
					50eb7a5f98 | ||
| 
						 | 
					484d1e7396 | ||
| 
						 | 
					a632e13334 | ||
| 
						 | 
					aa3f96f89c | ||
| 
						 | 
					16685ddb6c | ||
| 
						 | 
					e78b15b9f0 | ||
| 
						 | 
					35b0bd76f8 | ||
| 
						 | 
					1d7286c161 | ||
| 
						 | 
					5a7ec38ecd | ||
| 
						 | 
					ada4e3cdcd | ||
| 
						 | 
					ed62c87156 | ||
| 
						 | 
					840277f584 | ||
| 
						 | 
					ffddb7fb2b | ||
| 
						 | 
					b380421fd5 | ||
| 
						 | 
					20882a7598 | ||
| 
						 | 
					9d3dff47d2 | ||
| 
						 | 
					db5c7aba78 | ||
| 
						 | 
					e8e01aa60d | ||
| 
						 | 
					ae8226907f | ||
| 
						 | 
					803b66ae9d | ||
| 
						 | 
					20a508e2d6 | ||
| 
						 | 
					25ada1eae9 | ||
| 
						 | 
					808e4b38a3 | ||
| 
						 | 
					a496bc5a63 | ||
| 
						 | 
					c94713475f | ||
| 
						 | 
					d45c61109f | ||
| 
						 | 
					99220d72da | ||
| 
						 | 
					887eaef1aa | ||
| 
						 | 
					836a00e104 | ||
| 
						 | 
					8ee5061046 | ||
| 
						 | 
					61852b4021 | ||
| 
						 | 
					0b7de6f7b2 | ||
| 
						 | 
					9834a67cbd | ||
| 
						 | 
					d85a4a0c9f | ||
| 
						 | 
					67c8ec6d7e | ||
| 
						 | 
					2a2dd7ea19 | ||
| 
						 | 
					153e7ac7e4 | ||
| 
						 | 
					9420fd4946 | ||
| 
						 | 
					b14c5cd89c | ||
| 
						 | 
					4ab9141429 | ||
| 
						 | 
					769c2f9f49 | ||
| 
						 | 
					c41c498a9c | ||
| 
						 | 
					d1096582a5 | ||
| 
						 | 
					543989151f | ||
| 
						 | 
					7da83987e4 | ||
| 
						 | 
					523d553dac | ||
| 
						 | 
					ff6f0e9546 | ||
| 
						 | 
					3e63f6ba34 | ||
| 
						 | 
					811b92d24e | ||
| 
						 | 
					bc5ddc4541 | ||
| 
						 | 
					44e43729bf | ||
| 
						 | 
					203067c936 | ||
| 
						 | 
					f3b508c088 | ||
| 
						 | 
					081d84f848 | ||
| 
						 | 
					e381add944 | ||
| 
						 | 
					75d4eca722 | ||
| 
						 | 
					56904ad6b2 | ||
| 
						 | 
					b5ef552c25 | ||
| 
						 | 
					cbabb9392c | ||
| 
						 | 
					531b3dcf9e | ||
| 
						 | 
					d975daf3f0 | ||
| 
						 | 
					6137d551fe | ||
| 
						 | 
					cf625e3542 | ||
| 
						 | 
					e354fca4a4 | ||
| 
						 | 
					129e7afc16 | ||
| 
						 | 
					e83e0f6a33 | ||
| 
						 | 
					cf4f928b25 | ||
| 
						 | 
					e4d955e3f9 | ||
| 
						 | 
					13576087f4 | ||
| 
						 | 
					e74f6f3183 | ||
| 
						 | 
					8302d1d3c1 | ||
| 
						 | 
					b7320e6834 | ||
| 
						 | 
					3193fe0235 | ||
| 
						 | 
					7c2fa9f8a4 | ||
| 
						 | 
					e5f6133127 | ||
| 
						 | 
					0198c5b781 | ||
| 
						 | 
					f535073ae4 | ||
| 
						 | 
					b9895ecadd | ||
| 
						 | 
					322eb66fdf | ||
| 
						 | 
					f0abdc80eb | ||
| 
						 | 
					70a4f94c81 | ||
| 
						 | 
					f7e4b36746 | ||
| 
						 | 
					4e6b71ace5 | ||
| 
						 | 
					c916cd1a87 | ||
| 
						 | 
					442a529a72 | ||
| 
						 | 
					e1243f3d59 | ||
| 
						 | 
					62f8cd1db6 | ||
| 
						 | 
					af5f67d459 | ||
| 
						 | 
					13424a893e | ||
| 
						 | 
					30473ec41e | ||
| 
						 | 
					24d382c70d | ||
| 
						 | 
					3ddedc903e | ||
| 
						 | 
					e3c279b8be | ||
| 
						 | 
					d909b676c5 | ||
| 
						 | 
					08a8ac9971 | ||
| 
						 | 
					7073fd2f3b | ||
| 
						 | 
					a9f67a48a1 | ||
| 
						 | 
					fd058cc693 | ||
| 
						 | 
					c98df33003 | ||
| 
						 | 
					3f8b14f5f8 | ||
| 
						 | 
					1c335a68e0 | ||
| 
						 | 
					fb98050d9f | ||
| 
						 | 
					91cb7aa13d | ||
| 
						 | 
					ab0f7cc0c9 | ||
| 
						 | 
					b51f7f9a25 | ||
| 
						 | 
					d275e32e70 | ||
| 
						 | 
					22cf78d506 | ||
| 
						 | 
					c7c318b31e | ||
| 
						 | 
					a4d012828c | ||
| 
						 | 
					72cfebd2bc | ||
| 
						 | 
					a832cfb343 | ||
| 
						 | 
					9ef680db57 | ||
| 
						 | 
					2c09f4ef90 | ||
| 
						 | 
					c44454bf74 | ||
| 
						 | 
					ad1b9b7f6d | ||
| 
						 | 
					15e063e1b5 | ||
| 
						 | 
					c00a63e4c3 | ||
| 
						 | 
					dbda27b1d6 | ||
| 
						 | 
					9eef879dbe | ||
| 
						 | 
					cb29423636 | ||
| 
						 | 
					d754eecf07 | ||
| 
						 | 
					1e308782ee | ||
| 
						 | 
					69139cd85b | ||
| 
						 | 
					f59235bd5a | ||
| 
						 | 
					2930ba0457 | ||
| 
						 | 
					1513881eed | ||
| 
						 | 
					5e361f6748 | ||
| 
						 | 
					b182543a21 | ||
| 
						 | 
					cbce8b444f | ||
| 
						 | 
					38e92cfc62 | ||
| 
						 | 
					a7764dc6d5 | ||
| 
						 | 
					c74552c4c5 | ||
| 
						 | 
					7a0b437626 | ||
| 
						 | 
					c7e9f13d2e | ||
| 
						 | 
					51e816ad6d | ||
| 
						 | 
					dd047fd58f | ||
| 
						 | 
					300f023d2a | ||
| 
						 | 
					a282a80a6e | ||
| 
						 | 
					e668e17655 | ||
| 
						 | 
					a913d9728c | ||
| 
						 | 
					15862d393b | ||
| 
						 | 
					48a6cdd50a | ||
| 
						 | 
					09b05cde7f | ||
| 
						 | 
					da294038a8 | ||
| 
						 | 
					332f3fb8e8 | ||
| 
						 | 
					8e4743e719 | ||
| 
						 | 
					4a663e539a | ||
| 
						 | 
					98ac7ee277 | ||
| 
						 | 
					28c457730a | ||
| 
						 | 
					9b61fe9df2 | ||
| 
						 | 
					b55b01cb13 | ||
| 
						 | 
					90d8f3117f | ||
| 
						 | 
					c7e976c8c5 | ||
| 
						 | 
					6014b765f4 | ||
| 
						 | 
					ca295588c4 | ||
| 
						 | 
					437334355f | ||
| 
						 | 
					3432d4df29 | ||
| 
						 | 
					05fcb96181 | ||
| 
						 | 
					6bb27f90a4 | ||
| 
						 | 
					c10e8382a9 | ||
| 
						 | 
					6653a31eb7 | ||
| 
						 | 
					42561d04de | ||
| 
						 | 
					9a285ab935 | ||
| 
						 | 
					81771568be | ||
| 
						 | 
					795b43f174 | ||
| 
						 | 
					52203b50eb | ||
| 
						 | 
					0373b2c9dd | ||
| 
						 | 
					fe2c1c4ec6 | ||
| 
						 | 
					596b6542e8 | ||
| 
						 | 
					6897bf1254 | ||
| 
						 | 
					8a6a13e583 | ||
| 
						 | 
					b718285125 | ||
| 
						 | 
					e5ab918ef9 | ||
| 
						 | 
					6c6a2d08db | ||
| 
						 | 
					9e6617e3ca | ||
| 
						 | 
					94a50f92e1 | ||
| 
						 | 
					5c8be2a8f6 | ||
| 
						 | 
					1dcf2d80b4 | ||
| 
						 | 
					1197521921 | ||
| 
						 | 
					3863cfe786 | ||
| 
						 | 
					54bd07702c | ||
| 
						 | 
					a75e2b0c0e | ||
| 
						 | 
					29fd9b23fe | ||
| 
						 | 
					089e3b8946 | ||
| 
						 | 
					9c36fcec81 | ||
| 
						 | 
					74fa065266 | ||
| 
						 | 
					38f2495cf6 | ||
| 
						 | 
					4131fccbe0 | ||
| 
						 | 
					f2d748cfe4 | ||
| 
						 | 
					197ec0c29c | ||
| 
						 | 
					0a2af9335c | ||
| 
						 | 
					a52fa28ed1 | ||
| 
						 | 
					78ed24dbf6 | ||
| 
						 | 
					cda074fe24 | ||
| 
						 | 
					823032617d | ||
| 
						 | 
					5963459499 | ||
| 
						 | 
					0bc2c71b0c | ||
| 
						 | 
					b4e350e189 | ||
| 
						 | 
					941e46490f | ||
| 
						 | 
					4df92e903a | ||
| 
						 | 
					6ba02d0d50 | ||
| 
						 | 
					2dc122831b | ||
| 
						 | 
					f3f84e523a | ||
| 
						 | 
					0cdee25b5b | ||
| 
						 | 
					92b0314c14 | ||
| 
						 | 
					ad2bc7da96 | ||
| 
						 | 
					d8b606dc83 | ||
| 
						 | 
					5ce53dbcf4 | ||
| 
						 | 
					c5c1a9ab3c | ||
| 
						 | 
					564709aa98 | ||
| 
						 | 
					475158a145 | ||
| 
						 | 
					829df56733 | ||
| 
						 | 
					ee55f8790e | ||
| 
						 | 
					6d19fb3909 | ||
| 
						 | 
					9057712c8f | ||
| 
						 | 
					5d6e7de667 | ||
| 
						 | 
					8f6f70879c | ||
| 
						 | 
					3120087992 | ||
| 
						 | 
					0ec4cc223f | ||
| 
						 | 
					60c7be31b6 | ||
| 
						 | 
					2388f853c9 | ||
| 
						 | 
					1e8d4763bb | ||
| 
						 | 
					be4834688d | ||
| 
						 | 
					3937dad6a6 | ||
| 
						 | 
					463251dcc1 | ||
| 
						 | 
					3adca26808 | ||
| 
						 | 
					97a8bb52d6 | ||
| 
						 | 
					7748a980f9 | ||
| 
						 | 
					b044e274aa | ||
| 
						 | 
					6c3d4a11cc | ||
| 
						 | 
					98afd5516b | ||
| 
						 | 
					ea6926cad3 | ||
| 
						 | 
					3298961748 | ||
| 
						 | 
					0140f771c6 | ||
| 
						 | 
					64c4f512ce | ||
| 
						 | 
					5b1d45c1a9 | ||
| 
						 | 
					efbd1c15a9 | ||
| 
						 | 
					bce74890dc | ||
| 
						 | 
					eb93a43d13 | ||
| 
						 | 
					1dd75b63de | ||
| 
						 | 
					6caf79121b | ||
| 
						 | 
					5cdd34a388 | ||
| 
						 | 
					38c8ee8cd2 | ||
| 
						 | 
					d5c33a1183 | ||
| 
						 | 
					058e28911a | ||
| 
						 | 
					25a3bb351d | ||
| 
						 | 
					1607640bed | ||
| 
						 | 
					5f0cda829f | ||
| 
						 | 
					b003a374b8 | ||
| 
						 | 
					5f7c262759 | ||
| 
						 | 
					f3ab6b27c9 | ||
| 
						 | 
					d62eb56886 | ||
| 
						 | 
					c2724c6aad | ||
| 
						 | 
					0cde1683b6 | ||
| 
						 | 
					128f596f93 | ||
| 
						 | 
					27fae8eed0 | ||
| 
						 | 
					82ec4474c2 | ||
| 
						 | 
					0d85dae239 | ||
| 
						 | 
					7893693706 | ||
| 
						 | 
					25ce6af36e | ||
| 
						 | 
					c0269254e8 | ||
| 
						 | 
					2f2aefd48e | ||
| 
						 | 
					c05de45d99 | ||
| 
						 | 
					8f66da1128 | ||
| 
						 | 
					b5eaa8272b | ||
| 
						 | 
					7ee062e1de | ||
| 
						 | 
					8915af9b6d | ||
| 
						 | 
					ae1ef3215b | ||
| 
						 | 
					5d06fa217c | ||
| 
						 | 
					50b1f7db12 | ||
| 
						 | 
					fb82806956 | ||
| 
						 | 
					0658a93962 | ||
| 
						 | 
					f33be37070 | ||
| 
						 | 
					9278d7f851 | ||
| 
						 | 
					35ba898c97 | ||
| 
						 | 
					15f8d13d81 | ||
| 
						 | 
					946e508e8f | ||
| 
						 | 
					35b4125b98 | ||
| 
						 | 
					91d8f9d73e | ||
| 
						 | 
					c636993989 | ||
| 
						 | 
					65a24c16bc | ||
| 
						 | 
					15ce114440 | ||
| 
						 | 
					1722f75dcb | ||
| 
						 | 
					be597a551d | ||
| 
						 | 
					56bc945335 | ||
| 
						 | 
					fa9ceb5875 | ||
| 
						 | 
					0ccf5a16c7 | ||
| 
						 | 
					93c666b03d | ||
| 
						 | 
					d5c28b506c | ||
| 
						 | 
					3318f01aa1 | ||
| 
						 | 
					1641019091 | ||
| 
						 | 
					3572969866 | ||
| 
						 | 
					ad1fe8762b | ||
| 
						 | 
					c9c3e32a78 | ||
| 
						 | 
					f4c99c9cf7 | ||
| 
						 | 
					57447c24c8 | ||
| 
						 | 
					514ed03378 | ||
| 
						 | 
					078b039cf1 | ||
| 
						 | 
					a120670785 | ||
| 
						 | 
					9c05d136f5 | ||
| 
						 | 
					daeb8a67a3 | ||
| 
						 | 
					c64888cccd | ||
| 
						 | 
					6fc2637870 | ||
| 
						 | 
					fc4df23075 | ||
| 
						 | 
					3ae502986b | ||
| 
						 | 
					67f23d202a | ||
| 
						 | 
					3c38b9c93b | ||
| 
						 | 
					152a00baf5 | ||
| 
						 | 
					405d02023c | ||
| 
						 | 
					4ba2d0d7e0 | ||
| 
						 | 
					38ce230cf5 | ||
| 
						 | 
					c6696313bf | ||
| 
						 | 
					89717c9b15 | ||
| 
						 | 
					3aca699ad9 | ||
| 
						 | 
					f3ec9f02eb | ||
| 
						 | 
					1c7f3e6057 | ||
| 
						 | 
					db63a5a670 | ||
| 
						 | 
					0a2cccfd6e | ||
| 
						 | 
					113bd24796 | ||
| 
						 | 
					374a27a72e | ||
| 
						 | 
					567eff9ef4 | ||
| 
						 | 
					bfe33af03d | ||
| 
						 | 
					7ebc5eb70f | ||
| 
						 | 
					52287fd01d | ||
| 
						 | 
					e0f2bc2725 | ||
| 
						 | 
					4355ec3b44 | ||
| 
						 | 
					0c2f11ffd7 | ||
| 
						 | 
					65e2f38fac | ||
| 
						 | 
					aeaefbf8a9 | ||
| 
						 | 
					719cb3ae05 | ||
| 
						 | 
					08816c8f7d | ||
| 
						 | 
					1031203e3f | ||
| 
						 | 
					63ef6e7d3d | ||
| 
						 | 
					590c9bb978 | ||
| 
						 | 
					5055a8c7a3 | ||
| 
						 | 
					284c3f27e7 | ||
| 
						 | 
					468f198a6b | ||
| 
						 | 
					b0c6f6d4d3 | ||
| 
						 | 
					473061b4d5 | ||
| 
						 | 
					c4921f3da7 | ||
| 
						 | 
					91b871ef3b | ||
| 
						 | 
					698be6671c | ||
| 
						 | 
					47c546fafa | ||
| 
						 | 
					9d1a84858c | ||
| 
						 | 
					4dd5bf71ea | ||
| 
						 | 
					ea253e3bae | ||
| 
						 | 
					1d42e955fc | ||
| 
						 | 
					e636d486f5 | ||
| 
						 | 
					818cc545eb | ||
| 
						 | 
					e7858495e6 | ||
| 
						 | 
					af3a273a56 | ||
| 
						 | 
					6264c02543 | ||
| 
						 | 
					80d5bfd7c0 | ||
| 
						 | 
					3fa8b0ad14 | ||
| 
						 | 
					184a0b9481 | ||
| 
						 | 
					4e80096ee4 | ||
| 
						 | 
					0fb775d71a | ||
| 
						 | 
					76fdd047e7 | ||
| 
						 | 
					8590750e4c | ||
| 
						 | 
					590bd8e4bb | ||
| 
						 | 
					b4cb8c3d75 | ||
| 
						 | 
					21aa015a79 | ||
| 
						 | 
					dd5b9d420b | ||
| 
						 | 
					2593de5872 | ||
| 
						 | 
					d2ae740d5f | ||
| 
						 | 
					c56c6074e9 | ||
| 
						 | 
					426ce7fd35 | ||
| 
						 | 
					2a191aacb7 | ||
| 
						 | 
					50cd33dbb2 | ||
| 
						 | 
					e6b49a60c0 | ||
| 
						 | 
					a7e9356c16 | ||
| 
						 | 
					58cc1c8482 | ||
| 
						 | 
					88df4a2223 | ||
| 
						 | 
					d046100875 | ||
| 
						 | 
					0d4611052e | ||
| 
						 | 
					bdb03e07fc | ||
| 
						 | 
					1d790b9e8d | ||
| 
						 | 
					24bf15af4f | ||
| 
						 | 
					6410aa214e | ||
| 
						 | 
					a023308d52 | ||
| 
						 | 
					2516851056 | ||
| 
						 | 
					a3a77006ff | ||
| 
						 | 
					ebbd0128f1 | ||
| 
						 | 
					773641aa16 | ||
| 
						 | 
					57514e91b6 | ||
| 
						 | 
					a9d336baf5 | ||
| 
						 | 
					37da759fd5 | ||
| 
						 | 
					2ebc26953d | ||
| 
						 | 
					c8ba72b185 | ||
| 
						 | 
					35fe6e2f54 | ||
| 
						 | 
					f3d35e0ef3 | ||
| 
						 | 
					68327907a9 | ||
| 
						 | 
					44fe85b56d | ||
| 
						 | 
					72cbb156ae | ||
| 
						 | 
					5e274130a6 | ||
| 
						 | 
					94be03ec4f | ||
| 
						 | 
					6502855a70 | ||
| 
						 | 
					6bbdaf7ab0 | ||
| 
						 | 
					846e323840 | ||
| 
						 | 
					c198c33778 | ||
| 
						 | 
					c1cc3d1d1f | ||
| 
						 | 
					6b5e5a15d7 | ||
| 
						 | 
					7ac03b4d89 | ||
| 
						 | 
					6bd75fae33 | ||
| 
						 | 
					3cdaf62fa1 | ||
| 
						 | 
					9aea6c5585 | ||
| 
						 | 
					d533895637 | ||
| 
						 | 
					aa74a74c5c | ||
| 
						 | 
					46f0a256d7 | ||
| 
						 | 
					e33ad07e16 | ||
| 
						 | 
					786d62905d | ||
| 
						 | 
					75594b8fe5 | ||
| 
						 | 
					96d2f05eb7 | ||
| 
						 | 
					887f93181c | ||
| 
						 | 
					9f4a80f6ae | ||
| 
						 | 
					3e65ef3bea | ||
| 
						 | 
					4ca34e0436 | ||
| 
						 | 
					bb3f6ee086 | ||
| 
						 | 
					89f0a4875c | ||
| 
						 | 
					aa2be9b96c | ||
| 
						 | 
					707c1a2f7e | ||
| 
						 | 
					ed14a0029a | ||
| 
						 | 
					989661e4df | ||
| 
						 | 
					8874c687d8 | ||
| 
						 | 
					58e1997bcb | ||
| 
						 | 
					3e3055d7df | ||
| 
						 | 
					b68d6e9d1a | ||
| 
						 | 
					cf9d200b7c | ||
| 
						 | 
					e84da3089a | ||
| 
						 | 
					fee38b8d13 | ||
| 
						 | 
					e0a69a9e57 | ||
| 
						 | 
					0bdc95f9c4 | ||
| 
						 | 
					4c9e4e507c | ||
| 
						 | 
					adb50fe64f | ||
| 
						 | 
					e883aefcc3 | ||
| 
						 | 
					cd7e8bbd3e | ||
| 
						 | 
					99317f759b | ||
| 
						 | 
					7ff6a8651a | ||
| 
						 | 
					4e1e1d4fef | ||
| 
						 | 
					d34676c5b2 | ||
| 
						 | 
					4cf659c29b | ||
| 
						 | 
					ec61a5b32d | ||
| 
						 | 
					58f726c602 | ||
| 
						 | 
					bb0c3835f4 | ||
| 
						 | 
					e9642c7505 | ||
| 
						 | 
					f0b4ef5917 | ||
| 
						 | 
					1f12753c68 | ||
| 
						 | 
					0439d122a5 | ||
| 
						 | 
					4dad7f2ab6 | ||
| 
						 | 
					ce75dc502b | ||
| 
						 | 
					23f6c2e8c9 | ||
| 
						 | 
					d3461dd69b | ||
| 
						 | 
					05b1b8b240 | ||
| 
						 | 
					3118ba4466 | ||
| 
						 | 
					35cec0f1df | ||
| 
						 | 
					a19d238483 | ||
| 
						 | 
					a57fa2e9ad | ||
| 
						 | 
					c2b36cdffa | ||
| 
						 | 
					600b1814a1 | ||
| 
						 | 
					76e6957a8a | ||
| 
						 | 
					18df79ce00 | ||
| 
						 | 
					f14b413b7c | ||
| 
						 | 
					d0e73bd6b2 | ||
| 
						 | 
					f27b25a62e | ||
| 
						 | 
					6d8c7ba140 | ||
| 
						 | 
					af497c96ec | ||
| 
						 | 
					697c7a8dfe | ||
| 
						 | 
					3f5a189591 | ||
| 
						 | 
					bcb18ff2f4 | ||
| 
						 | 
					b1ba3df989 | ||
| 
						 | 
					203ac0970d | ||
| 
						 | 
					e5329dc28a | ||
| 
						 | 
					f8ef6278a5 | ||
| 
						 | 
					7f13a8d2bc | ||
| 
						 | 
					b0b078c0fb | ||
| 
						 | 
					c282433095 | ||
| 
						 | 
					8d7f3bd215 | ||
| 
						 | 
					f6c268dc1e | ||
| 
						 | 
					48f25b0799 | ||
| 
						 | 
					50cfbaaab5 | ||
| 
						 | 
					de775511d0 | ||
| 
						 | 
					a524a60c46 | ||
| 
						 | 
					6cf2fa02e5 | ||
| 
						 | 
					1a8cb877db | ||
| 
						 | 
					33727aad62 | ||
| 
						 | 
					ac79d810d0 | ||
| 
						 | 
					2b912c6834 | ||
| 
						 | 
					cf2404743d | ||
| 
						 | 
					38bffd423c | ||
| 
						 | 
					42711d76d6 | ||
| 
						 | 
					789f3d993c | ||
| 
						 | 
					d5376ab090 | ||
| 
						 | 
					62ad7e5ad3 | ||
| 
						 | 
					f0cb2fc21a | ||
| 
						 | 
					3fe521421f | ||
| 
						 | 
					85445c4ef2 | ||
| 
						 | 
					b4faf3faad | ||
| 
						 | 
					5f2745c32a | ||
| 
						 | 
					9ef6713aa1 | ||
| 
						 | 
					736c66f46a | ||
| 
						 | 
					98b699c483 | ||
| 
						 | 
					93044590cc | ||
| 
						 | 
					30676d118f | ||
| 
						 | 
					e306ac0197 | ||
| 
						 | 
					65c6b4af82 | ||
| 
						 | 
					a402f646fe | ||
| 
						 | 
					db29fed99a | ||
| 
						 | 
					94a2104b55 | ||
| 
						 | 
					0f886a1ece | ||
| 
						 | 
					3db024b24d | ||
| 
						 | 
					75bf75d552 | ||
| 
						 | 
					5bbe59c9a2 | ||
| 
						 | 
					28e447ea4a | ||
| 
						 | 
					31a874e24e | ||
| 
						 | 
					987412db51 | ||
| 
						 | 
					94ab5c7abf | ||
| 
						 | 
					d358fb256b | ||
| 
						 | 
					e00652ce86 | ||
| 
						 | 
					086effa6cb | ||
| 
						 | 
					de000a8b4e | ||
| 
						 | 
					8f75317820 | ||
| 
						 | 
					dbb016c9e4 | ||
| 
						 | 
					2831b91e94 | ||
| 
						 | 
					923b2594df | ||
| 
						 | 
					44874fb5e0 | ||
| 
						 | 
					5b3d7ccb46 | ||
| 
						 | 
					daecd3efa1 | ||
| 
						 | 
					86e4b58117 | ||
| 
						 | 
					34426d86dc | ||
| 
						 | 
					50a915b7b6 | ||
| 
						 | 
					775ba2596a | ||
| 
						 | 
					7141962cce | ||
| 
						 | 
					d5a4527e9d | ||
| 
						 | 
					cf775e3487 | ||
| 
						 | 
					44e1bed57a | ||
| 
						 | 
					915ba07f86 | ||
| 
						 | 
					a852c5d0c3 | ||
| 
						 | 
					8cde6cd4d7 | ||
| 
						 | 
					a70d59eb45 | ||
| 
						 | 
					06534fa0ae | ||
| 
						 | 
					8a548ef252 | ||
| 
						 | 
					99c854ce1d | ||
| 
						 | 
					46fe3c520c | ||
| 
						 | 
					8568fc3544 | ||
| 
						 | 
					5ba0aef799 | ||
| 
						 | 
					71cbf86b2c | ||
| 
						 | 
					aed6b34950 | ||
| 
						 | 
					e9076c1748 | ||
| 
						 | 
					8b0cf7d248 | ||
| 
						 | 
					9aa794248f | ||
| 
						 | 
					b357e2ecef | ||
| 
						 | 
					68e27e9513 | ||
| 
						 | 
					7b4e4c2172 | ||
| 
						 | 
					9e602eb575 | ||
| 
						 | 
					4618c624c8 | ||
| 
						 | 
					5979bdd48e | ||
| 
						 | 
					2170392bdf | ||
| 
						 | 
					e9335d9508 | ||
| 
						 | 
					5f444c1c82 | ||
| 
						 | 
					a3de277c43 | ||
| 
						 | 
					b5b8593e7f | ||
| 
						 | 
					03163d6a61 | ||
| 
						 | 
					bd90caa99d | ||
| 
						 | 
					cada0aa70b | ||
| 
						 | 
					cbde357d94 | ||
| 
						 | 
					75488e4bd5 | ||
| 
						 | 
					d1bdf4a292 | ||
| 
						 | 
					c73c063ad5 | ||
| 
						 | 
					9b000ff242 | ||
| 
						 | 
					a282937468 | ||
| 
						 | 
					6da3aab046 | ||
| 
						 | 
					ff2589c97f | ||
| 
						 | 
					a9f000e7ef | ||
| 
						 | 
					d9be63e6cb | ||
| 
						 | 
					1a626a68f0 | ||
| 
						 | 
					330504b91e | ||
| 
						 | 
					6bb0166055 | ||
| 
						 | 
					b22988e6b8 | ||
| 
						 | 
					f7edac961f | ||
| 
						 | 
					5b9b120fa6 | ||
| 
						 | 
					f07e4fc87f | ||
| 
						 | 
					46eb870c46 | ||
| 
						 | 
					a0e192b6e4 | ||
| 
						 | 
					dc3fa6c780 | ||
| 
						 | 
					dd5604f5d9 | ||
| 
						 | 
					170936a96e | ||
| 
						 | 
					93c9974019 | ||
| 
						 | 
					377579e802 | ||
| 
						 | 
					881cf082c2 | ||
| 
						 | 
					bfc924bc2a | ||
| 
						 | 
					dbd92b2db9 | ||
| 
						 | 
					505a68093d | ||
| 
						 | 
					f2b81a2f23 | ||
| 
						 | 
					c49dbab127 | ||
| 
						 | 
					36adfe87fb | ||
| 
						 | 
					cdfcf0f068 | ||
| 
						 | 
					c3676091ee | ||
| 
						 | 
					ec19b86ade | ||
| 
						 | 
					ec43f4e6ab | ||
| 
						 | 
					7bf74c6a5d | ||
| 
						 | 
					cbb50c14e1 | ||
| 
						 | 
					d42622a5c1 | ||
| 
						 | 
					f9bee1485b | ||
| 
						 | 
					ff72e8abab | ||
| 
						 | 
					c87eee1fda | ||
| 
						 | 
					960aa90c32 | ||
| 
						 | 
					5a10b2a75f | ||
| 
						 | 
					72b553efe8 | ||
| 
						 | 
					c4210be3c7 | ||
| 
						 | 
					28a49827ff | ||
| 
						 | 
					736869454b | ||
| 
						 | 
					db9084b0dc | ||
| 
						 | 
					c39d75b448 | ||
| 
						 | 
					71a546dd44 | ||
| 
						 | 
					96e3d3a22c | ||
| 
						 | 
					0ad91101a4 | ||
| 
						 | 
					e9df58709e | ||
| 
						 | 
					a80dcaa1c3 | ||
| 
						 | 
					d29b7fa1c7 | ||
| 
						 | 
					89551774db | ||
| 
						 | 
					be58d3afb6 | ||
| 
						 | 
					652d803739 | ||
| 
						 | 
					2688914125 | ||
| 
						 | 
					1d489cfcea | ||
| 
						 | 
					cb55ce084c | ||
| 
						 | 
					dae7da0e4e | ||
| 
						 | 
					e4630e6a0f | ||
| 
						 | 
					6d9abf11b8 | ||
| 
						 | 
					7e8def50aa | ||
| 
						 | 
					2b7f72deec | ||
| 
						 | 
					9b1f25140e | ||
| 
						 | 
					f4caa0029e | ||
| 
						 | 
					15e046b3ce | ||
| 
						 | 
					9d0485fa22 | ||
| 
						 | 
					eeef92b068 | ||
| 
						 | 
					a1418fe33c | ||
| 
						 | 
					8f21e736dd | ||
| 
						 | 
					222301307f | ||
| 
						 | 
					1b19fdfe11 | ||
| 
						 | 
					1f2ef1cdb7 | ||
| 
						 | 
					c394b21423 | ||
| 
						 | 
					696e84ea88 | ||
| 
						 | 
					4a17dca7b9 | ||
| 
						 | 
					b3f38f3264 | ||
| 
						 | 
					9bcfe6461d | ||
| 
						 | 
					4a82a91f2d | ||
| 
						 | 
					24cd905911 | ||
| 
						 | 
					6d62ab4257 | ||
| 
						 | 
					f6ff32f339 | ||
| 
						 | 
					bc523d302b | ||
| 
						 | 
					1facbb2906 | ||
| 
						 | 
					a5eb87e835 | ||
| 
						 | 
					8265436437 | ||
| 
						 | 
					96545bd523 | ||
| 
						 | 
					dd5ee68808 | ||
| 
						 | 
					e773ed2912 | ||
| 
						 | 
					9949fc4646 | ||
| 
						 | 
					d88da1f6ab | ||
| 
						 | 
					fe8e3f2bcf | ||
| 
						 | 
					4b9d753254 | ||
| 
						 | 
					5ba385b74d | ||
| 
						 | 
					96c0a5c911 | ||
| 
						 | 
					ec655f5182 | ||
| 
						 | 
					093df395c7 | ||
| 
						 | 
					92d2373d77 | ||
| 
						 | 
					319959ad6e | ||
| 
						 | 
					de1e4b4c6c | ||
| 
						 | 
					c2e79d22d2 | ||
| 
						 | 
					596c9b1d27 | ||
| 
						 | 
					40223e6b3f | ||
| 
						 | 
					eec1dd6448 | ||
| 
						 | 
					6c1261d28d | ||
| 
						 | 
					4697ea1c1a | ||
| 
						 | 
					6e20031dce | ||
| 
						 | 
					a5fe9bc6d6 | ||
| 
						 | 
					637660df2a | ||
| 
						 | 
					e1549d109e | ||
| 
						 | 
					418d270d74 | ||
| 
						 | 
					5f8fc3d155 | ||
| 
						 | 
					fce3b3ce7b | ||
| 
						 | 
					074bd9f045 | ||
| 
						 | 
					b1ea26467d | ||
| 
						 | 
					48ebd74859 | ||
| 
						 | 
					ef5b7ce853 | ||
| 
						 | 
					e0053d57f7 | ||
| 
						 | 
					06268543d0 | ||
| 
						 | 
					58eadd6d7b | ||
| 
						 | 
					f250594e97 | ||
| 
						 | 
					c1b6828ed4 | ||
| 
						 | 
					328ecd1cfb | ||
| 
						 | 
					14b3f300ae | ||
| 
						 | 
					435e82c824 | ||
| 
						 | 
					aeda7520fe | ||
| 
						 | 
					f54fb177da | ||
| 
						 | 
					56ef8e3ebf | ||
| 
						 | 
					717c123b82 | ||
| 
						 | 
					132f6c8420 | ||
| 
						 | 
					116e16e30d | ||
| 
						 | 
					5dbb6afc60 | ||
| 
						 | 
					ae8050a3f7 | ||
| 
						 | 
					8870e966a6 | ||
| 
						 | 
					f5a5cffdec | ||
| 
						 | 
					220c622f8f | ||
| 
						 | 
					e509749421 | ||
| 
						 | 
					a69cec89fb | ||
| 
						 | 
					8f5c289818 | ||
| 
						 | 
					5e544891aa | ||
| 
						 | 
					1aaf4ae5bc | ||
| 
						 | 
					9f3188fe45 | ||
| 
						 | 
					b2fc7d476a | ||
| 
						 | 
					c37885e743 | ||
| 
						 | 
					0209ace221 | ||
| 
						 | 
					a9c6c681ce | ||
| 
						 | 
					b0cd8579f1 | ||
| 
						 | 
					ba0753c418 | ||
| 
						 | 
					d58545d340 | ||
| 
						 | 
					98de486534 | ||
| 
						 | 
					662b46101c | ||
| 
						 | 
					23287e9b47 | ||
| 
						 | 
					d5bd77cca1 | ||
| 
						 | 
					bfad3df3b3 | ||
| 
						 | 
					4fcb1498c2 | ||
| 
						 | 
					8a92904287 | ||
| 
						 | 
					6a74d62e98 | ||
| 
						 | 
					78568ef7c0 | ||
| 
						 | 
					35ccfb14c2 | ||
| 
						 | 
					735afc64f6 | ||
| 
						 | 
					b178bd6bd6 | ||
| 
						 | 
					b3a0b252ad | ||
| 
						 | 
					424799191c | ||
| 
						 | 
					faafb45d83 | ||
| 
						 | 
					75b3561594 | 
							
								
								
									
										14
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "presets": [
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      "next/babel",
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "preset-env": {
 | 
				
			||||||
 | 
					          "targets": {
 | 
				
			||||||
 | 
					            "browsers": ["> 0.25%, not dead"]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										97
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					# Logs
 | 
				
			||||||
 | 
					logs
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					npm-debug.log*
 | 
				
			||||||
 | 
					yarn-debug.log*
 | 
				
			||||||
 | 
					yarn-error.log*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Runtime data
 | 
				
			||||||
 | 
					pids
 | 
				
			||||||
 | 
					*.pid
 | 
				
			||||||
 | 
					*.seed
 | 
				
			||||||
 | 
					*.pid.lock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Directory for instrumented libs generated by jscoverage/JSCover
 | 
				
			||||||
 | 
					lib-cov
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Coverage directory used by tools like istanbul
 | 
				
			||||||
 | 
					coverage
 | 
				
			||||||
 | 
					*.lcov
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# nyc test coverage
 | 
				
			||||||
 | 
					.nyc_output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
 | 
				
			||||||
 | 
					.grunt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Node.js dependencies
 | 
				
			||||||
 | 
					/node_modules
 | 
				
			||||||
 | 
					/jspm_packages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TypeScript v1 declaration files
 | 
				
			||||||
 | 
					typings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Optional npm cache directory
 | 
				
			||||||
 | 
					.npm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Optional eslint cache
 | 
				
			||||||
 | 
					.eslintcache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Optional REPL history
 | 
				
			||||||
 | 
					.node_repl_history
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Output of 'npm pack'
 | 
				
			||||||
 | 
					*.tgz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Yarn Integrity file
 | 
				
			||||||
 | 
					.yarn-integrity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# dotenv environment variable files
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					.env.test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# local env files
 | 
				
			||||||
 | 
					.env*.local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Next.js build output
 | 
				
			||||||
 | 
					.next
 | 
				
			||||||
 | 
					out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Nuxt.js build output
 | 
				
			||||||
 | 
					.nuxt
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Gatsby files
 | 
				
			||||||
 | 
					.cache/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Vuepress build output
 | 
				
			||||||
 | 
					.vuepress/dist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Serverless directories
 | 
				
			||||||
 | 
					.serverless/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# FuseBox cache
 | 
				
			||||||
 | 
					.fusebox/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# DynamoDB Local files
 | 
				
			||||||
 | 
					.dynamodb/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Temporary folders
 | 
				
			||||||
 | 
					tmp
 | 
				
			||||||
 | 
					temp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# IDE and editor directories
 | 
				
			||||||
 | 
					.idea
 | 
				
			||||||
 | 
					.vscode
 | 
				
			||||||
 | 
					*.swp
 | 
				
			||||||
 | 
					*.swo
 | 
				
			||||||
 | 
					*~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# OS generated files
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					Thumbs.db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# secret key
 | 
				
			||||||
 | 
					*.key
 | 
				
			||||||
 | 
					*.key.pub
 | 
				
			||||||
							
								
								
									
										89
									
								
								.env.template
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								.env.template
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					# Your openai api key. (required)
 | 
				
			||||||
 | 
					OPENAI_API_KEY=sk-xxxx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# DeepSeek Api Key. (Optional)
 | 
				
			||||||
 | 
					DEEPSEEK_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Access password, separated by comma. (optional)
 | 
				
			||||||
 | 
					CODE=your-password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# You can start service behind a proxy. (optional)
 | 
				
			||||||
 | 
					PROXY_URL=http://localhost:7890
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Enable MCP functionality (optional)
 | 
				
			||||||
 | 
					# Default: Empty (disabled)
 | 
				
			||||||
 | 
					# Set to "true" to enable MCP functionality
 | 
				
			||||||
 | 
					ENABLE_MCP=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (optional)
 | 
				
			||||||
 | 
					# Default: Empty
 | 
				
			||||||
 | 
					# Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
 | 
				
			||||||
 | 
					GOOGLE_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (optional)
 | 
				
			||||||
 | 
					# Default: https://generativelanguage.googleapis.com/
 | 
				
			||||||
 | 
					# Google Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
 | 
				
			||||||
 | 
					GOOGLE_URL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Override openai api request base url. (optional)
 | 
				
			||||||
 | 
					# Default: https://api.openai.com
 | 
				
			||||||
 | 
					# Examples: http://your-openai-proxy.com
 | 
				
			||||||
 | 
					BASE_URL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Specify OpenAI organization ID.(optional)
 | 
				
			||||||
 | 
					# Default: Empty
 | 
				
			||||||
 | 
					OPENAI_ORG_ID=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (optional)
 | 
				
			||||||
 | 
					# Default: Empty
 | 
				
			||||||
 | 
					# If you do not want users to use GPT-4, set this value to 1.
 | 
				
			||||||
 | 
					DISABLE_GPT4=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (optional)
 | 
				
			||||||
 | 
					# Default: Empty
 | 
				
			||||||
 | 
					# If you do not want users to input their own API key, set this value to 1.
 | 
				
			||||||
 | 
					HIDE_USER_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (optional)
 | 
				
			||||||
 | 
					# Default: Empty
 | 
				
			||||||
 | 
					# If you do want users to query balance, set this value to 1.
 | 
				
			||||||
 | 
					ENABLE_BALANCE_QUERY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (optional)
 | 
				
			||||||
 | 
					# Default: Empty
 | 
				
			||||||
 | 
					# If you want to disable parse settings from url, set this value to 1.
 | 
				
			||||||
 | 
					DISABLE_FAST_LINK=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (optional)
 | 
				
			||||||
 | 
					# Default: Empty
 | 
				
			||||||
 | 
					# To control custom models, use + to add a custom model, use - to hide a model, use name=displayName to customize model name, separated by comma.
 | 
				
			||||||
 | 
					CUSTOM_MODELS=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# (optional)
 | 
				
			||||||
 | 
					# Default: Empty
 | 
				
			||||||
 | 
					# Change default model
 | 
				
			||||||
 | 
					DEFAULT_MODEL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# anthropic claude Api Key.(optional)
 | 
				
			||||||
 | 
					ANTHROPIC_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### anthropic claude Api version. (optional)
 | 
				
			||||||
 | 
					ANTHROPIC_API_VERSION=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### anthropic claude Api url (optional)
 | 
				
			||||||
 | 
					ANTHROPIC_URL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### (optional)
 | 
				
			||||||
 | 
					WHITE_WEBDAV_ENDPOINTS=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### siliconflow Api key (optional)
 | 
				
			||||||
 | 
					SILICONFLOW_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### siliconflow Api url (optional)
 | 
				
			||||||
 | 
					SILICONFLOW_URL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### ppio Api key (optional)
 | 
				
			||||||
 | 
					PPIO_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### ppio Api url (optional)
 | 
				
			||||||
 | 
					PPIO_URL=
 | 
				
			||||||
@@ -1 +1,3 @@
 | 
				
			|||||||
public/serviceWorker.js
 | 
					public/serviceWorker.js
 | 
				
			||||||
 | 
					app/mcp/mcp_config.json
 | 
				
			||||||
 | 
					app/mcp/mcp_config.default.json
 | 
				
			||||||
@@ -1,4 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "extends": "next/core-web-vitals",
 | 
					  "extends": "next/core-web-vitals",
 | 
				
			||||||
  "plugins": ["prettier"]
 | 
					  "plugins": ["prettier", "unused-imports"],
 | 
				
			||||||
 | 
					  "rules": {
 | 
				
			||||||
 | 
					    "unused-imports/no-unused-imports": "warn"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										80
									
								
								.github/ISSUE_TEMPLATE/1_bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								.github/ISSUE_TEMPLATE/1_bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					name: '🐛 Bug Report'
 | 
				
			||||||
 | 
					description: 'Report an bug'
 | 
				
			||||||
 | 
					title: '[Bug] '
 | 
				
			||||||
 | 
					labels: ['bug']
 | 
				
			||||||
 | 
					body:
 | 
				
			||||||
 | 
					  - type: dropdown
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📦 Deployment Method'
 | 
				
			||||||
 | 
					      multiple: true
 | 
				
			||||||
 | 
					      options:
 | 
				
			||||||
 | 
					        - 'Official installation package'
 | 
				
			||||||
 | 
					        - 'Vercel'
 | 
				
			||||||
 | 
					        - 'Zeabur'
 | 
				
			||||||
 | 
					        - 'Sealos'
 | 
				
			||||||
 | 
					        - 'Netlify'
 | 
				
			||||||
 | 
					        - 'Docker'
 | 
				
			||||||
 | 
					        - 'Other'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📌 Version'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  - type: dropdown
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '💻 Operating System'
 | 
				
			||||||
 | 
					      multiple: true
 | 
				
			||||||
 | 
					      options:
 | 
				
			||||||
 | 
					        - 'Windows'
 | 
				
			||||||
 | 
					        - 'macOS'
 | 
				
			||||||
 | 
					        - 'Ubuntu'
 | 
				
			||||||
 | 
					        - 'Other Linux'
 | 
				
			||||||
 | 
					        - 'iOS'
 | 
				
			||||||
 | 
					        - 'iPad OS'
 | 
				
			||||||
 | 
					        - 'Android'
 | 
				
			||||||
 | 
					        - 'Other'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📌 System Version'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: dropdown
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🌐 Browser'
 | 
				
			||||||
 | 
					      multiple: true
 | 
				
			||||||
 | 
					      options:
 | 
				
			||||||
 | 
					        - 'Chrome'
 | 
				
			||||||
 | 
					        - 'Edge'
 | 
				
			||||||
 | 
					        - 'Safari'
 | 
				
			||||||
 | 
					        - 'Firefox'
 | 
				
			||||||
 | 
					        - 'Other'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📌 Browser Version'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🐛 Bug Description'
 | 
				
			||||||
 | 
					      description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📷 Recurrence Steps'
 | 
				
			||||||
 | 
					      description: A clear and concise description of how to recurrence.
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🚦 Expected Behavior'
 | 
				
			||||||
 | 
					      description: A clear and concise description of what you expected to happen.
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📝 Additional Information'
 | 
				
			||||||
 | 
					      description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
 | 
				
			||||||
							
								
								
									
										80
									
								
								.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					name: '🐛 反馈缺陷'
 | 
				
			||||||
 | 
					description: '反馈一个问题/缺陷'
 | 
				
			||||||
 | 
					title: '[Bug] '
 | 
				
			||||||
 | 
					labels: ['bug']
 | 
				
			||||||
 | 
					body:
 | 
				
			||||||
 | 
					  - type: dropdown
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📦 部署方式'
 | 
				
			||||||
 | 
					      multiple: true
 | 
				
			||||||
 | 
					      options:
 | 
				
			||||||
 | 
					        - '官方安装包'
 | 
				
			||||||
 | 
					        - 'Vercel'
 | 
				
			||||||
 | 
					        - 'Zeabur'
 | 
				
			||||||
 | 
					        - 'Sealos'
 | 
				
			||||||
 | 
					        - 'Netlify'
 | 
				
			||||||
 | 
					        - 'Docker'
 | 
				
			||||||
 | 
					        - 'Other'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📌 软件版本'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - type: dropdown
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '💻 系统环境'
 | 
				
			||||||
 | 
					      multiple: true
 | 
				
			||||||
 | 
					      options:
 | 
				
			||||||
 | 
					        - 'Windows'
 | 
				
			||||||
 | 
					        - 'macOS'
 | 
				
			||||||
 | 
					        - 'Ubuntu'
 | 
				
			||||||
 | 
					        - 'Other Linux'
 | 
				
			||||||
 | 
					        - 'iOS'
 | 
				
			||||||
 | 
					        - 'iPad OS'
 | 
				
			||||||
 | 
					        - 'Android'
 | 
				
			||||||
 | 
					        - 'Other'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📌 系统版本'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: dropdown
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🌐 浏览器'
 | 
				
			||||||
 | 
					      multiple: true
 | 
				
			||||||
 | 
					      options:
 | 
				
			||||||
 | 
					        - 'Chrome'
 | 
				
			||||||
 | 
					        - 'Edge'
 | 
				
			||||||
 | 
					        - 'Safari'
 | 
				
			||||||
 | 
					        - 'Firefox'
 | 
				
			||||||
 | 
					        - 'Other'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📌 浏览器版本'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🐛 问题描述'
 | 
				
			||||||
 | 
					      description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📷 复现步骤'
 | 
				
			||||||
 | 
					      description: 请提供一个清晰且简洁的描述,说明如何复现问题。
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🚦 期望结果'
 | 
				
			||||||
 | 
					      description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📝 补充信息'
 | 
				
			||||||
 | 
					      description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。
 | 
				
			||||||
							
								
								
									
										21
									
								
								.github/ISSUE_TEMPLATE/2_feature_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/ISSUE_TEMPLATE/2_feature_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					name: '🌠 Feature Request'
 | 
				
			||||||
 | 
					description: 'Suggest an idea'
 | 
				
			||||||
 | 
					title: '[Feature Request] '
 | 
				
			||||||
 | 
					labels: ['enhancement']
 | 
				
			||||||
 | 
					body:
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🥰 Feature Description'
 | 
				
			||||||
 | 
					      description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🧐 Proposed Solution'
 | 
				
			||||||
 | 
					      description: Describe the solution you'd like in a clear and concise manner.
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📝 Additional Information'
 | 
				
			||||||
 | 
					      description: Add any other context about the problem here.
 | 
				
			||||||
							
								
								
									
										21
									
								
								.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					name: '🌠 功能需求'
 | 
				
			||||||
 | 
					description: '提出需求或建议'
 | 
				
			||||||
 | 
					title: '[Feature Request] '
 | 
				
			||||||
 | 
					labels: ['enhancement']
 | 
				
			||||||
 | 
					body:
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🥰 需求描述'
 | 
				
			||||||
 | 
					      description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '🧐 解决方案'
 | 
				
			||||||
 | 
					      description: 请清晰且简洁地描述您想要的解决方案。
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: '📝 补充信息'
 | 
				
			||||||
 | 
					      description: 在这里添加关于问题的任何其他背景信息。
 | 
				
			||||||
							
								
								
									
										43
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										43
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -1,43 +0,0 @@
 | 
				
			|||||||
---
 | 
					 | 
				
			||||||
name: Bug report
 | 
					 | 
				
			||||||
about: Create a report to help us improve
 | 
					 | 
				
			||||||
title: "[Bug] "
 | 
					 | 
				
			||||||
labels: ''
 | 
					 | 
				
			||||||
assignees: ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Describe the bug**
 | 
					 | 
				
			||||||
A clear and concise description of what the bug is.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**To Reproduce**
 | 
					 | 
				
			||||||
Steps to reproduce the behavior:
 | 
					 | 
				
			||||||
1. Go to '...'
 | 
					 | 
				
			||||||
2. Click on '....'
 | 
					 | 
				
			||||||
3. Scroll down to '....'
 | 
					 | 
				
			||||||
4. See error
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Expected behavior**
 | 
					 | 
				
			||||||
A clear and concise description of what you expected to happen.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Screenshots**
 | 
					 | 
				
			||||||
If applicable, add screenshots to help explain your problem.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Deployment**
 | 
					 | 
				
			||||||
- [ ] Docker
 | 
					 | 
				
			||||||
- [ ] Vercel
 | 
					 | 
				
			||||||
- [ ] Server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Desktop (please complete the following information):**
 | 
					 | 
				
			||||||
 - OS: [e.g. iOS]
 | 
					 | 
				
			||||||
 - Browser [e.g. chrome, safari]
 | 
					 | 
				
			||||||
 - Version [e.g. 22]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Smartphone (please complete the following information):**
 | 
					 | 
				
			||||||
 - Device: [e.g. iPhone6]
 | 
					 | 
				
			||||||
 - OS: [e.g. iOS8.1]
 | 
					 | 
				
			||||||
 - Browser [e.g. stock browser, safari]
 | 
					 | 
				
			||||||
 - Version [e.g. 22]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Additional Logs**
 | 
					 | 
				
			||||||
Add any logs about the problem here.
 | 
					 | 
				
			||||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							@@ -1,20 +0,0 @@
 | 
				
			|||||||
---
 | 
					 | 
				
			||||||
name: Feature request
 | 
					 | 
				
			||||||
about: Suggest an idea for this project
 | 
					 | 
				
			||||||
title: "[Feature] "
 | 
					 | 
				
			||||||
labels: ''
 | 
					 | 
				
			||||||
assignees: ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Is your feature request related to a problem? Please describe.**
 | 
					 | 
				
			||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Describe the solution you'd like**
 | 
					 | 
				
			||||||
A clear and concise description of what you want to happen.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Describe alternatives you've considered**
 | 
					 | 
				
			||||||
A clear and concise description of any alternative solutions or features you've considered.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Additional context**
 | 
					 | 
				
			||||||
Add any other context or screenshots about the feature request here.
 | 
					 | 
				
			||||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/功能建议.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/功能建议.md
									
									
									
									
										vendored
									
									
								
							@@ -1,20 +0,0 @@
 | 
				
			|||||||
---
 | 
					 | 
				
			||||||
name: 功能建议
 | 
					 | 
				
			||||||
about: 请告诉我们你的灵光一闪
 | 
					 | 
				
			||||||
title: "[Feature] "
 | 
					 | 
				
			||||||
labels: ''
 | 
					 | 
				
			||||||
assignees: ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**这个功能与现有的问题有关吗?**
 | 
					 | 
				
			||||||
如果有关,请在此列出链接或者描述问题。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**你想要什么功能或者有什么建议?**
 | 
					 | 
				
			||||||
尽管告诉我们。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**有没有可以参考的同类竞品?**
 | 
					 | 
				
			||||||
可以给出参考产品的链接或者截图。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**其他信息**
 | 
					 | 
				
			||||||
可以说说你的其他考虑。
 | 
					 | 
				
			||||||
							
								
								
									
										32
									
								
								.github/ISSUE_TEMPLATE/反馈问题.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								.github/ISSUE_TEMPLATE/反馈问题.md
									
									
									
									
										vendored
									
									
								
							@@ -1,32 +0,0 @@
 | 
				
			|||||||
---
 | 
					 | 
				
			||||||
name: 反馈问题
 | 
					 | 
				
			||||||
about: 请告诉我们你遇到的问题
 | 
					 | 
				
			||||||
title: "[Bug] "
 | 
					 | 
				
			||||||
labels: ''
 | 
					 | 
				
			||||||
assignees: ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**反馈须知**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
请在下方中括号内输入 x 来表示你已经知晓相关内容。
 | 
					 | 
				
			||||||
- [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答;
 | 
					 | 
				
			||||||
- [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。
 | 
					 | 
				
			||||||
- [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**描述问题**
 | 
					 | 
				
			||||||
请在此描述你遇到了什么问题。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**如何复现**
 | 
					 | 
				
			||||||
请告诉我们你是通过什么操作触发的该问题。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**截图**
 | 
					 | 
				
			||||||
请在此提供控制台截图、屏幕截图或者服务端的 log 截图。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**一些必要的信息**
 | 
					 | 
				
			||||||
 - 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16]
 | 
					 | 
				
			||||||
 - 浏览器: [比如 chrome, safari]
 | 
					 | 
				
			||||||
 - 版本: [填写设置页面的版本号]
 | 
					 | 
				
			||||||
 - 部署方式:[比如 vercel、docker 或者服务器部署]
 | 
					 | 
				
			||||||
							
								
								
									
										28
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					#### 💻 变更类型 | Change Type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- For change type, change [ ] to [x]. -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [ ] feat    <!-- 引入新功能 | Introduce new features -->
 | 
				
			||||||
 | 
					- [ ] fix    <!-- 修复 Bug | Fix a bug -->
 | 
				
			||||||
 | 
					- [ ] refactor    <!-- 重构代码(既不修复 Bug 也不添加新功能) | Refactor code that neither fixes a bug nor adds a feature -->
 | 
				
			||||||
 | 
					- [ ] perf    <!-- 提升性能的代码变更 | A code change that improves performance -->
 | 
				
			||||||
 | 
					- [ ] style    <!-- 添加或更新不影响代码含义的样式文件 | Add or update style files that do not affect the meaning of the code -->
 | 
				
			||||||
 | 
					- [ ] test    <!-- 添加缺失的测试或纠正现有的测试 | Adding missing tests or correcting existing tests -->
 | 
				
			||||||
 | 
					- [ ] docs    <!-- 仅文档更新 | Documentation only changes -->
 | 
				
			||||||
 | 
					- [ ] ci    <!-- 修改持续集成配置文件和脚本 | Changes to our CI configuration files and scripts -->
 | 
				
			||||||
 | 
					- [ ] chore    <!-- 其他不修改 src 或 test 文件的变更 | Other changes that don’t modify src or test files -->
 | 
				
			||||||
 | 
					- [ ] build    <!-- 进行架构变更 | Make architectural changes -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 🔀 变更说明 | Description of Change
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- 
 | 
				
			||||||
 | 
					感谢您的 Pull Request ,请提供此 Pull Request 的变更说明
 | 
				
			||||||
 | 
					Thank you for your Pull Request. Please provide a description above.
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 📝 补充信息 | Additional Information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- 
 | 
				
			||||||
 | 
					请添加与此 Pull Request 相关的补充信息
 | 
				
			||||||
 | 
					Add any other context about the Pull Request here.
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
							
								
								
									
										11
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					# To get started with Dependabot version updates, you'll need to specify which
 | 
				
			||||||
 | 
					# package ecosystems to update and where the package manifests are located.
 | 
				
			||||||
 | 
					# Please see the documentation for all configuration options:
 | 
				
			||||||
 | 
					# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					version: 2
 | 
				
			||||||
 | 
					updates:
 | 
				
			||||||
 | 
					  - package-ecosystem: "npm" # See documentation for possible values
 | 
				
			||||||
 | 
					    directory: "/" # Location of package manifests
 | 
				
			||||||
 | 
					    schedule:
 | 
				
			||||||
 | 
					      interval: "weekly"
 | 
				
			||||||
							
								
								
									
										110
									
								
								.github/workflows/app.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								.github/workflows/app.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					name: Release App
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					  release:
 | 
				
			||||||
 | 
					    types: [published]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  create-release:
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      contents: write
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    outputs:
 | 
				
			||||||
 | 
					      release_id: ${{ steps.create-release.outputs.result }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
 | 
					      - name: setup node
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: 18
 | 
				
			||||||
 | 
					      - name: get version
 | 
				
			||||||
 | 
					        run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
 | 
				
			||||||
 | 
					      - name: create release
 | 
				
			||||||
 | 
					        id: create-release
 | 
				
			||||||
 | 
					        uses: actions/github-script@v6
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          script: |
 | 
				
			||||||
 | 
					            const { data } = await github.rest.repos.getLatestRelease({
 | 
				
			||||||
 | 
					              owner: context.repo.owner,
 | 
				
			||||||
 | 
					              repo: context.repo.repo,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            return data.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  build-tauri:
 | 
				
			||||||
 | 
					    needs: create-release
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      contents: write
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        config:
 | 
				
			||||||
 | 
					          - os: ubuntu-latest
 | 
				
			||||||
 | 
					            arch: x86_64
 | 
				
			||||||
 | 
					            rust_target: x86_64-unknown-linux-gnu
 | 
				
			||||||
 | 
					          - os: macos-latest
 | 
				
			||||||
 | 
					            arch: aarch64
 | 
				
			||||||
 | 
					            rust_target: x86_64-apple-darwin,aarch64-apple-darwin
 | 
				
			||||||
 | 
					          - os: windows-latest
 | 
				
			||||||
 | 
					            arch: x86_64
 | 
				
			||||||
 | 
					            rust_target: x86_64-pc-windows-msvc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    runs-on: ${{ matrix.config.os }}
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
 | 
					      - name: setup node
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: 18
 | 
				
			||||||
 | 
					          cache: 'yarn'
 | 
				
			||||||
 | 
					      - name: install Rust stable
 | 
				
			||||||
 | 
					        uses: dtolnay/rust-toolchain@stable
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          targets: ${{ matrix.config.rust_target }}
 | 
				
			||||||
 | 
					      - uses: Swatinem/rust-cache@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          key: ${{ matrix.config.os }}
 | 
				
			||||||
 | 
					      - name: install dependencies (ubuntu only)
 | 
				
			||||||
 | 
					        if: matrix.config.os == 'ubuntu-latest'
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          sudo apt-get update
 | 
				
			||||||
 | 
					          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
 | 
				
			||||||
 | 
					      - name: install frontend dependencies
 | 
				
			||||||
 | 
					        run: yarn install # change this to npm or pnpm depending on which one you use
 | 
				
			||||||
 | 
					      - uses: tauri-apps/tauri-action@v0
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					          TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
 | 
				
			||||||
 | 
					          TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
 | 
				
			||||||
 | 
					          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
 | 
				
			||||||
 | 
					          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
 | 
				
			||||||
 | 
					          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
 | 
				
			||||||
 | 
					          APPLE_ID: ${{ secrets.APPLE_ID }}
 | 
				
			||||||
 | 
					          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
 | 
				
			||||||
 | 
					          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          releaseId: ${{ needs.create-release.outputs.release_id }}
 | 
				
			||||||
 | 
					          args: ${{ matrix.config.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  publish-release:
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      contents: write
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    needs: [create-release, build-tauri]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: publish release
 | 
				
			||||||
 | 
					        id: publish-release
 | 
				
			||||||
 | 
					        uses: actions/github-script@v6
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          release_id: ${{ needs.create-release.outputs.release_id }}
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          script: |
 | 
				
			||||||
 | 
					            github.rest.repos.updateRelease({
 | 
				
			||||||
 | 
					              owner: context.repo.owner,
 | 
				
			||||||
 | 
					              repo: context.repo.repo,
 | 
				
			||||||
 | 
					              release_id: process.env.release_id,
 | 
				
			||||||
 | 
					              draft: false,
 | 
				
			||||||
 | 
					              prerelease: false
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
							
								
								
									
										82
									
								
								.github/workflows/deploy_preview.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								.github/workflows/deploy_preview.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					name: VercelPreviewDeployment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  pull_request_target:
 | 
				
			||||||
 | 
					    types:
 | 
				
			||||||
 | 
					      - review_requested
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
 | 
				
			||||||
 | 
					  VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
 | 
				
			||||||
 | 
					  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
 | 
				
			||||||
 | 
					  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
 | 
				
			||||||
 | 
					  VERCEL_PR_DOMAIN_SUFFIX: ${{ secrets.VERCEL_PR_DOMAIN_SUFFIX }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					permissions:
 | 
				
			||||||
 | 
					  contents: read
 | 
				
			||||||
 | 
					  statuses: write
 | 
				
			||||||
 | 
					  pull-requests: write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  deploy-preview:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Extract branch name
 | 
				
			||||||
 | 
					        shell: bash
 | 
				
			||||||
 | 
					        run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
 | 
				
			||||||
 | 
					        id: extract_branch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Hash branch name
 | 
				
			||||||
 | 
					        uses: pplanel/hash-calculator-action@v1.3.1
 | 
				
			||||||
 | 
					        id: hash_branch
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          input: ${{ steps.extract_branch.outputs.branch }}
 | 
				
			||||||
 | 
					          method: MD5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Set Environment Variables
 | 
				
			||||||
 | 
					        id: set_env
 | 
				
			||||||
 | 
					        if: github.event_name == 'pull_request_target'
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          echo "VERCEL_ALIAS_DOMAIN=${{ github.event.pull_request.number }}-${{ github.workflow }}.${VERCEL_PR_DOMAIN_SUFFIX}" >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Install Vercel CLI
 | 
				
			||||||
 | 
					        run: npm install --global vercel@latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Cache dependencies
 | 
				
			||||||
 | 
					        uses: actions/cache@v4
 | 
				
			||||||
 | 
					        id: cache-npm
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: ~/.npm
 | 
				
			||||||
 | 
					          key: npm-${{ hashFiles('package-lock.json') }}
 | 
				
			||||||
 | 
					          restore-keys: npm-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Pull Vercel Environment Information
 | 
				
			||||||
 | 
					        run: vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Deploy Project Artifacts to Vercel
 | 
				
			||||||
 | 
					        id: vercel
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          META_TAG: ${{ steps.hash_branch.outputs.digest }}-${{ github.run_number }}-${{ github.run_attempt}}
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          set -e
 | 
				
			||||||
 | 
					          vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
 | 
				
			||||||
 | 
					          vercel build --token=${VERCEL_TOKEN}
 | 
				
			||||||
 | 
					          vercel deploy --prebuilt --archive=tgz --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          DEFAULT_URL=$(vercel ls --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }})
 | 
				
			||||||
 | 
					          ALIAS_URL=$(vercel alias set ${DEFAULT_URL} ${{ steps.set_env.outputs.VERCEL_ALIAS_DOMAIN }} --token=${VERCEL_TOKEN} --scope ${VERCEL_TEAM}| awk '{print $3}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          echo "New preview URL: ${DEFAULT_URL}"
 | 
				
			||||||
 | 
					          echo "New alias URL: ${ALIAS_URL}"
 | 
				
			||||||
 | 
					          echo "VERCEL_URL=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - uses: mshick/add-pr-comment@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          message: |
 | 
				
			||||||
 | 
					            Your build has completed!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            [Preview deployment](${{ steps.vercel.outputs.VERCEL_URL }})
 | 
				
			||||||
							
								
								
									
										15
									
								
								.github/workflows/issue-translator.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/issue-translator.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					name: Issue Translator
 | 
				
			||||||
 | 
					on: 
 | 
				
			||||||
 | 
					  issue_comment: 
 | 
				
			||||||
 | 
					    types: [created]
 | 
				
			||||||
 | 
					  issues: 
 | 
				
			||||||
 | 
					    types: [opened]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: usthe/issues-translate-action@v2.7
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          IS_MODIFY_TITLE: false
 | 
				
			||||||
 | 
					          CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.
 | 
				
			||||||
							
								
								
									
										40
									
								
								.github/workflows/remove_deploy_preview.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/workflows/remove_deploy_preview.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					name: Removedeploypreview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					permissions:
 | 
				
			||||||
 | 
					  contents: read
 | 
				
			||||||
 | 
					  statuses: write
 | 
				
			||||||
 | 
					  pull-requests: write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
 | 
				
			||||||
 | 
					  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
 | 
				
			||||||
 | 
					  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  pull_request_target:
 | 
				
			||||||
 | 
					    types:
 | 
				
			||||||
 | 
					      - closed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  delete-deployments:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Extract branch name
 | 
				
			||||||
 | 
					        shell: bash
 | 
				
			||||||
 | 
					        run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					        id: extract_branch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Hash branch name
 | 
				
			||||||
 | 
					        uses: pplanel/hash-calculator-action@v1.3.1
 | 
				
			||||||
 | 
					        id: hash_branch
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          input: ${{ steps.extract_branch.outputs.branch }}
 | 
				
			||||||
 | 
					          method: MD5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Call the delete-deployment-preview.sh script
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          META_TAG: ${{ steps.hash_branch.outputs.digest }}
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          bash ./scripts/delete-deployment-preview.sh
 | 
				
			||||||
							
								
								
									
										8
									
								
								.github/workflows/sync.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/sync.yml
									
									
									
									
										vendored
									
									
								
							@@ -5,7 +5,7 @@ permissions:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  schedule:
 | 
					  schedule:
 | 
				
			||||||
    - cron: "0 * * * *" # every hour
 | 
					    - cron: "0 0 * * *" # every day
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
@@ -24,7 +24,7 @@ jobs:
 | 
				
			|||||||
        id: sync
 | 
					        id: sync
 | 
				
			||||||
        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
 | 
					        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
 | 
					          upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web
 | 
				
			||||||
          upstream_sync_branch: main
 | 
					          upstream_sync_branch: main
 | 
				
			||||||
          target_sync_branch: main
 | 
					          target_sync_branch: main
 | 
				
			||||||
          target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
 | 
					          target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
 | 
				
			||||||
@@ -35,6 +35,6 @@ jobs:
 | 
				
			|||||||
      - name: Sync check
 | 
					      - name: Sync check
 | 
				
			||||||
        if: failure()
 | 
					        if: failure()
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。"
 | 
					          echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0"
 | 
				
			||||||
          echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]."
 | 
					          echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates"
 | 
				
			||||||
          exit 1
 | 
					          exit 1
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										39
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					name: Run Tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - main
 | 
				
			||||||
 | 
					    tags:
 | 
				
			||||||
 | 
					      - "!*"
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    types:
 | 
				
			||||||
 | 
					      - review_requested
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  test:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Checkout repository
 | 
				
			||||||
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Set up Node.js
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: 18
 | 
				
			||||||
 | 
					          cache: "yarn"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Cache node_modules
 | 
				
			||||||
 | 
					        uses: actions/cache@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: node_modules
 | 
				
			||||||
 | 
					          key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
 | 
				
			||||||
 | 
					          restore-keys: |
 | 
				
			||||||
 | 
					            ${{ runner.os }}-node_modules-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Install dependencies
 | 
				
			||||||
 | 
					        run: yarn install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Run Jest tests
 | 
				
			||||||
 | 
					        run: yarn test:ci
 | 
				
			||||||
							
								
								
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -36,7 +36,16 @@ yarn-error.log*
 | 
				
			|||||||
next-env.d.ts
 | 
					next-env.d.ts
 | 
				
			||||||
dev
 | 
					dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public/prompts.json
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vscode
 | 
					.vscode
 | 
				
			||||||
.idea
 | 
					.idea
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# docker-compose env files
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*.key
 | 
				
			||||||
 | 
					*.key.pub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					masks.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mcp config
 | 
				
			||||||
 | 
					app/mcp/mcp_config.json
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -8,7 +8,7 @@ WORKDIR /app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
COPY package.json yarn.lock ./
 | 
					COPY package.json yarn.lock ./
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN yarn config set registry 'https://registry.npm.taobao.org'
 | 
					RUN yarn config set registry 'https://registry.npmmirror.com/'
 | 
				
			||||||
RUN yarn install
 | 
					RUN yarn install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FROM base AS builder
 | 
					FROM base AS builder
 | 
				
			||||||
@@ -16,6 +16,7 @@ FROM base AS builder
 | 
				
			|||||||
RUN apk update && apk add --no-cache git
 | 
					RUN apk update && apk add --no-cache git
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV OPENAI_API_KEY=""
 | 
					ENV OPENAI_API_KEY=""
 | 
				
			||||||
 | 
					ENV GOOGLE_API_KEY=""
 | 
				
			||||||
ENV CODE=""
 | 
					ENV CODE=""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /app
 | 
					WORKDIR /app
 | 
				
			||||||
@@ -31,16 +32,22 @@ RUN apk add proxychains-ng
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ENV PROXY_URL=""
 | 
					ENV PROXY_URL=""
 | 
				
			||||||
ENV OPENAI_API_KEY=""
 | 
					ENV OPENAI_API_KEY=""
 | 
				
			||||||
 | 
					ENV GOOGLE_API_KEY=""
 | 
				
			||||||
ENV CODE=""
 | 
					ENV CODE=""
 | 
				
			||||||
 | 
					ENV ENABLE_MCP=""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY --from=builder /app/public ./public
 | 
					COPY --from=builder /app/public ./public
 | 
				
			||||||
COPY --from=builder /app/.next/standalone ./
 | 
					COPY --from=builder /app/.next/standalone ./
 | 
				
			||||||
COPY --from=builder /app/.next/static ./.next/static
 | 
					COPY --from=builder /app/.next/static ./.next/static
 | 
				
			||||||
COPY --from=builder /app/.next/server ./.next/server
 | 
					COPY --from=builder /app/.next/server ./.next/server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp
 | 
				
			||||||
 | 
					COPY --from=builder /app/app/mcp/mcp_config.default.json /app/app/mcp/mcp_config.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
EXPOSE 3000
 | 
					EXPOSE 3000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CMD if [ -n "$PROXY_URL" ]; then \
 | 
					CMD if [ -n "$PROXY_URL" ]; then \
 | 
				
			||||||
 | 
					    export HOSTNAME="0.0.0.0"; \
 | 
				
			||||||
    protocol=$(echo $PROXY_URL | cut -d: -f1); \
 | 
					    protocol=$(echo $PROXY_URL | cut -d: -f1); \
 | 
				
			||||||
    host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
 | 
					    host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
 | 
				
			||||||
    port=$(echo $PROXY_URL | cut -d: -f3); \
 | 
					    port=$(echo $PROXY_URL | cut -d: -f3); \
 | 
				
			||||||
@@ -50,6 +57,8 @@ CMD if [ -n "$PROXY_URL" ]; then \
 | 
				
			|||||||
    echo "remote_dns_subnet 224" >> $conf; \
 | 
					    echo "remote_dns_subnet 224" >> $conf; \
 | 
				
			||||||
    echo "tcp_read_time_out 15000" >> $conf; \
 | 
					    echo "tcp_read_time_out 15000" >> $conf; \
 | 
				
			||||||
    echo "tcp_connect_time_out 8000" >> $conf; \
 | 
					    echo "tcp_connect_time_out 8000" >> $conf; \
 | 
				
			||||||
 | 
					    echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \
 | 
				
			||||||
 | 
					    echo "localnet ::1/128" >> $conf; \
 | 
				
			||||||
    echo "[ProxyList]" >> $conf; \
 | 
					    echo "[ProxyList]" >> $conf; \
 | 
				
			||||||
    echo "$protocol $host $port" >> $conf; \
 | 
					    echo "$protocol $host $port" >> $conf; \
 | 
				
			||||||
    cat /etc/proxychains.conf; \
 | 
					    cat /etc/proxychains.conf; \
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										88
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,75 +1,21 @@
 | 
				
			|||||||
版权所有(c)<2023><Zhang Yifei>
 | 
					MIT License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
反996许可证版本1.0
 | 
					Copyright (c) 2023-2025 NextChat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
在符合下列条件的情况下,
 | 
					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:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
 | 
					copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1.	个人或法人实体必须在许可作品的每个再散布或衍生副本上包含以上版权声明和本许可证,不
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
    得自行修改。
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
				
			||||||
2.	个人或法人实体必须严格遵守与个人实际所在地或个人出生地或归化地、或法人实体注册地或
 | 
					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
 | 
				
			||||||
3.	个人或法人不得以任何方式诱导或强迫其全职或兼职员工或其独立承包人以口头或书面形式同
 | 
					SOFTWARE.
 | 
				
			||||||
    意直接或间接限制、削弱或放弃其所拥有的,受相关与劳动和就业有关的法律、法规、规则和
 | 
					 | 
				
			||||||
    标准保护的权利或补救措施,无论该等书面或口头协议是否被该司法管辖区的法律所承认,该
 | 
					 | 
				
			||||||
    等个人或法人实体也不得以任何方法限制其雇员或独立承包人向版权持有人或监督许可证合规
 | 
					 | 
				
			||||||
    情况的有关当局报告或投诉上述违反许可证的行为的权利。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
该授权作品是"按原样"提供,不做任何明示或暗示的保证,包括但不限于对适销性、特定用途适用
 | 
					 | 
				
			||||||
性和非侵权性的保证。在任何情况下,无论是在合同诉讼、侵权诉讼或其他诉讼中,版权持有人均
 | 
					 | 
				
			||||||
不承担因本软件或本软件的使用或其他交易而产生、引起或与之相关的任何索赔、损害或其他责任。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
------------------------- ENGLISH ------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Copyright (c) <2023> <Zhang Yifei>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Anti 996 License Version 1.0 (Draft)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Permission is hereby granted to any individual or legal entity obtaining a copy
 | 
					 | 
				
			||||||
of this licensed work (including the source code, documentation and/or related
 | 
					 | 
				
			||||||
items, hereinafter collectively referred to as the "licensed work"), free of
 | 
					 | 
				
			||||||
charge, to deal with the licensed work for any purpose, including without
 | 
					 | 
				
			||||||
limitation, the rights to use, reproduce, modify, prepare derivative works of,
 | 
					 | 
				
			||||||
publish, distribute and sublicense the licensed work, subject to the following
 | 
					 | 
				
			||||||
conditions:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1.  The individual or the legal entity must conspicuously display, without
 | 
					 | 
				
			||||||
    modification, this License on each redistributed or derivative copy of the
 | 
					 | 
				
			||||||
    Licensed Work.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2.  The individual or the legal entity must strictly comply with all applicable
 | 
					 | 
				
			||||||
    laws, regulations, rules and standards of the jurisdiction relating to
 | 
					 | 
				
			||||||
    labor and employment where the individual is physically located or where
 | 
					 | 
				
			||||||
    the individual was born or naturalized; or where the legal entity is
 | 
					 | 
				
			||||||
    registered or is operating (whichever is stricter). In case that the
 | 
					 | 
				
			||||||
    jurisdiction has no such laws, regulations, rules and standards or its
 | 
					 | 
				
			||||||
    laws, regulations, rules and standards are unenforceable, the individual
 | 
					 | 
				
			||||||
    or the legal entity are required to comply with Core International Labor
 | 
					 | 
				
			||||||
    Standards.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3.  The individual or the legal entity shall not induce or force its
 | 
					 | 
				
			||||||
    employee(s), whether full-time or part-time, or its independent
 | 
					 | 
				
			||||||
    contractor(s), in any methods, to agree in oral or written form,
 | 
					 | 
				
			||||||
    to directly or indirectly restrict, weaken or relinquish his or
 | 
					 | 
				
			||||||
    her rights or remedies under such laws, regulations, rules and
 | 
					 | 
				
			||||||
    standards relating to labor and employment as mentioned above,
 | 
					 | 
				
			||||||
    no matter whether such written or oral agreement are enforceable
 | 
					 | 
				
			||||||
    under the laws of the said jurisdiction, nor shall such individual
 | 
					 | 
				
			||||||
    or the legal entity limit, in any methods, the rights of its employee(s)
 | 
					 | 
				
			||||||
    or independent contractor(s) from reporting or complaining to the copyright
 | 
					 | 
				
			||||||
    holder or relevant authorities monitoring the compliance of the license
 | 
					 | 
				
			||||||
    about its violation(s) of the said license.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
THE LICENSED WORK 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 COPYRIGHT
 | 
					 | 
				
			||||||
HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 | 
					 | 
				
			||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION
 | 
					 | 
				
			||||||
WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										433
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										433
									
								
								README.md
									
									
									
									
									
								
							@@ -1,93 +1,126 @@
 | 
				
			|||||||
<div align="center">
 | 
					<div align="center">
 | 
				
			||||||
<img src="./docs/images/icon.svg" alt="icon"/>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h1 align="center">ChatGPT Next Web</h1>
 | 
					<a href='https://nextchat.club'>
 | 
				
			||||||
 | 
					  <img src="https://github.com/user-attachments/assets/83bdcc07-ae5e-4954-a53a-ac151ba6ccf3" width="1000" alt="icon"/>
 | 
				
			||||||
 | 
					</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h1 align="center">NextChat</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
English / [简体中文](./README_CN.md)
 | 
					English / [简体中文](./README_CN.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
One-Click to deploy well-designed ChatGPT web UI on Vercel.
 | 
					<a href="https://trendshift.io/repositories/5973" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5973" alt="ChatGPTNextWeb%2FChatGPT-Next-Web | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 | 
				
			||||||
 | 
					 | 
				
			||||||
一键免费部署你的私人 ChatGPT 网页应用。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[Demo](https://chat-gpt-next-web.vercel.app/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[演示](https://chat-gpt-next-web.vercel.app/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/234462588-e8eff256-f5ca-46ef-8f5f-d7db6d28735a.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
 | 
					✨ Light and Fast AI Assistant,with Claude, DeepSeek, GPT4 & Gemini Pro support. 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[![Saas][Saas-image]][saas-url]
 | 
				
			||||||
 | 
					[![Web][Web-image]][web-url]
 | 
				
			||||||
 | 
					[![Windows][Windows-image]][download-url]
 | 
				
			||||||
 | 
					[![MacOS][MacOS-image]][download-url]
 | 
				
			||||||
 | 
					[![Linux][Linux-image]][download-url]
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					[NextChatAI](https://nextchat.club?utm_source=readme) / [iOS APP](https://apps.apple.com/us/app/nextchat-ai/id6743085599) / [Web App Demo](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Enterprise Edition](#enterprise-edition) 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[saas-url]: https://nextchat.club?utm_source=readme
 | 
				
			||||||
 | 
					[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
 | 
				
			||||||
 | 
					[web-url]: https://app.nextchat.dev/
 | 
				
			||||||
 | 
					[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
 | 
				
			||||||
 | 
					[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
 | 
				
			||||||
 | 
					[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows
 | 
				
			||||||
 | 
					[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
 | 
				
			||||||
 | 
					[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://vercel.com/button" alt="Deploy on Vercel" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/ChatGPTNextWeb/NextChat) 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="50" width="" >](https://monica.im/?utm=nxcrp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 🥳 Cheer for NextChat iOS Version Online!
 | 
				
			||||||
 | 
					> [👉 Click Here to Install Now](https://apps.apple.com/us/app/nextchat-ai/id6743085599)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [❤️ Source Code Coming Soon](https://github.com/ChatGPTNextWeb/NextChat-iOS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					## 🫣 NextChat Support MCP  ! 
 | 
				
			||||||
 | 
					> Before build, please set env ENABLE_MCP=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<img src="https://github.com/user-attachments/assets/d8851f40-4e36-4335-b1a4-ec1e11488c7e"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Enterprise Edition
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Meeting Your Company's Privatization and Customization Deployment Requirements:
 | 
				
			||||||
 | 
					- **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image.
 | 
				
			||||||
 | 
					- **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members.
 | 
				
			||||||
 | 
					- **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel.
 | 
				
			||||||
 | 
					- **Knowledge Integration**: Combining your internal knowledge base with AI capabilities, making it more relevant to your company's specific business needs compared to general AI.
 | 
				
			||||||
 | 
					- **Security Auditing**: Automatically intercept sensitive inquiries and trace all historical conversation records, ensuring AI adherence to corporate information security standards.
 | 
				
			||||||
 | 
					- **Private Deployment**: Enterprise-level private deployment supporting various mainstream private cloud solutions, ensuring data security and privacy protection.
 | 
				
			||||||
 | 
					- **Continuous Updates**: Ongoing updates and upgrades in cutting-edge capabilities like multimodal AI, ensuring consistent innovation and advancement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Screenshots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Features
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- **Deploy for free with one-click** on Vercel in under 1 minute
 | 
					- **Deploy for free with one-click** on Vercel in under 1 minute
 | 
				
			||||||
- Privacy first, all data stored locally in the browser
 | 
					- Compact client (~5MB) on Linux/Windows/MacOS, [download it now](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
 | 
				
			||||||
 | 
					- Fully compatible with self-deployed LLMs, recommended for use with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) or [LocalAI](https://github.com/go-skynet/LocalAI)
 | 
				
			||||||
 | 
					- Privacy first, all data is stored locally in the browser
 | 
				
			||||||
 | 
					- Markdown support: LaTex, mermaid, code highlight, etc.
 | 
				
			||||||
- Responsive design, dark mode and PWA
 | 
					- Responsive design, dark mode and PWA
 | 
				
			||||||
- Fast first screen loading speed (~100kb), support streaming response
 | 
					- Fast first screen loading speed (~100kb), support streaming response
 | 
				
			||||||
- New in v2: create, share and debug your chat tools with prompt templates (mask)
 | 
					- New in v2: create, share and debug your chat tools with prompt templates (mask)
 | 
				
			||||||
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
 | 
					- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
 | 
				
			||||||
- Automatically compresses chat history to support long conversations while also saving your tokens
 | 
					- Automatically compresses chat history to support long conversations while also saving your tokens
 | 
				
			||||||
- One-click export all chat history with full Markdown support
 | 
					- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
 | 
				
			||||||
- I18n supported
 | 
					
 | 
				
			||||||
 | 
					<div align="center">
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Roadmap
 | 
					## Roadmap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
 | 
					- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
 | 
				
			||||||
- [x] User Prompt: user can edit and save custom prompts to prompt list
 | 
					- [x] User Prompt: user can edit and save custom prompts to prompt list
 | 
				
			||||||
- [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
 | 
					- [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
 | 
				
			||||||
- [ ] Share as image, share to ShareGPT
 | 
					- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
 | 
				
			||||||
- [ ] Desktop App with tauri
 | 
					- [x] Desktop App with tauri
 | 
				
			||||||
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc.
 | 
					- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
 | 
				
			||||||
- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
					- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
 | 
				
			||||||
 | 
					- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
 | 
				
			||||||
### Not in Plan
 | 
					  - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
 | 
				
			||||||
 | 
					- [x] Supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
 | 
				
			||||||
- User login, accounts, cloud sync
 | 
					- [ ] local knowledge base
 | 
				
			||||||
- UI text customize
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## What's New
 | 
					## What's New
 | 
				
			||||||
 | 
					- 🚀 v2.15.8 Now supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
 | 
				
			||||||
 | 
					- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
 | 
				
			||||||
 | 
					- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
 | 
				
			||||||
 | 
					- 🚀 v2.14.0 Now supports  Artifacts & SD 
 | 
				
			||||||
 | 
					- 🚀 v2.10.1 support Google Gemini Pro model.
 | 
				
			||||||
 | 
					- 🚀 v2.9.11 you can use azure endpoint now.
 | 
				
			||||||
 | 
					- 🚀 v2.8 now we have a client that runs across all platforms!
 | 
				
			||||||
 | 
					- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
 | 
				
			||||||
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
 | 
					- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 主要功能
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- 在 1 分钟内使用 Vercel **免费一键部署**
 | 
					 | 
				
			||||||
- 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
 | 
					 | 
				
			||||||
- 极快的首屏加载速度(~100kb),支持流式响应
 | 
					 | 
				
			||||||
- 隐私安全,所有数据保存在用户浏览器本地
 | 
					 | 
				
			||||||
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
 | 
					 | 
				
			||||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
 | 
					 | 
				
			||||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
 | 
					 | 
				
			||||||
- 一键导出聊天记录,完整的 Markdown 支持
 | 
					 | 
				
			||||||
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 开发计划
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
 | 
					 | 
				
			||||||
- [x] 允许用户自行编辑内置 Prompt 列表
 | 
					 | 
				
			||||||
- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
 | 
					 | 
				
			||||||
- [ ] 分享为图片,分享到 ShareGPT
 | 
					 | 
				
			||||||
- [ ] 使用 tauri 打包桌面应用
 | 
					 | 
				
			||||||
- [ ] 支持自部署的大语言模型
 | 
					 | 
				
			||||||
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### 不会开发的功能
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- 界面文字自定义
 | 
					 | 
				
			||||||
- 用户登录、账号管理、消息云同步
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 最新动态
 | 
					 | 
				
			||||||
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Get Started
 | 
					## Get Started
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何开始使用](./README_CN.md#开始使用)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
					1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
				
			||||||
2. Click
 | 
					2. Click
 | 
				
			||||||
   [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web), remember that `CODE` is your page password;
 | 
					   [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web), remember that `CODE` is your page password;
 | 
				
			||||||
@@ -95,14 +128,10 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## FAQ
 | 
					## FAQ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[简体中文 > 常见问题](./docs/faq-cn.md)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[English > FAQ](./docs/faq-en.md)
 | 
					[English > FAQ](./docs/faq-en.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Keep Updated
 | 
					## Keep Updated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何保持代码更新](./README_CN.md#保持更新)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
 | 
					If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
We recommend that you follow the steps below to re-deploy:
 | 
					We recommend that you follow the steps below to re-deploy:
 | 
				
			||||||
@@ -113,7 +142,7 @@ We recommend that you follow the steps below to re-deploy:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Enable Automatic Updates
 | 
					### Enable Automatic Updates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> If you encounter a failure of Upstream Sync execution, please manually sync fork once.
 | 
					> If you encounter a failure of Upstream Sync execution, please [manually update code](./README.md#manually-updating-code).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
 | 
					After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -125,12 +154,10 @@ After forking the project, due to the limitations imposed by GitHub, you need to
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
 | 
					If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You can star or watch this project or follow author to get release notifictions in time.
 | 
					You can star or watch this project or follow author to get release notifications in time.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Access Password
 | 
					## Access Password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何增加访问密码](./README_CN.md#配置页面访问密码)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
 | 
					This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
@@ -141,15 +168,13 @@ After adding or modifying this environment variable, please redeploy the project
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Environment Variables
 | 
					## Environment Variables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
 | 
					### `CODE` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Access password, separated by comma.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `OPENAI_API_KEY` (required)
 | 
					### `OPENAI_API_KEY` (required)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Your openai api key.
 | 
					Your openai api key, join multiple api keys with comma.
 | 
				
			||||||
 | 
					 | 
				
			||||||
### `CODE` (optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Access passsword, separated by comma.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `BASE_URL` (optional)
 | 
					### `BASE_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -163,9 +188,193 @@ Override openai api request base url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Specify OpenAI organization ID.
 | 
					Specify OpenAI organization ID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `AZURE_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Example: https://{azure-resource-url}/openai
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure deploy url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `AZURE_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `AZURE_API_VERSION` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `GOOGLE_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Google Gemini Pro Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `GOOGLE_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Google Gemini Pro Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					anthropic claude Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_API_VERSION` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					anthropic claude Api version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					anthropic claude Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_SECRET_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu Secret Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BYTEDANCE_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ByteDance Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BYTEDANCE_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ByteDance Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ALIBABA_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Alibaba Cloud Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ALIBABA_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Alibaba Cloud Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_URL` (Optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					iflytek Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_API_KEY` (Optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					iflytek Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_API_SECRET` (Optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					iflytek Api Secret.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CHATGLM_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ChatGLM Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CHATGLM_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ChatGLM Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEEPSEEK_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DeepSeek Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEEPSEEK_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DeepSeek Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `HIDE_USER_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Default: Empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you do not want users to input their own API key, set this value to 1.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DISABLE_GPT4` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Default: Empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you do not want users to use GPT-4, set this value to 1.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ENABLE_BALANCE_QUERY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Default: Empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you do want users to query balance, set this value to 1.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DISABLE_FAST_LINK` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Default: Empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you want to disable parse settings from url, set this to 1.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CUSTOM_MODELS` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Default: Empty
 | 
				
			||||||
 | 
					> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name=displayName` to customize model name, separated by comma.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					User `-all` to disable all default models, `+all` to enable all default models.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
 | 
				
			||||||
 | 
					> Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
 | 
				
			||||||
 | 
					> If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
 | 
				
			||||||
 | 
					> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEFAULT_MODEL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Change default model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `VISION_MODELS` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Default: Empty
 | 
				
			||||||
 | 
					> Example: `gpt-4-vision,claude-3-opus,my-custom-model` means add vision capabilities to these models in addition to the default pattern matches (which detect models containing keywords like "vision", "claude-3", "gemini-1.5", etc).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Add additional models to have vision capabilities, beyond the default pattern matching. Multiple models should be separated by commas.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `WHITE_WEBDAV_ENDPOINTS` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
 | 
				
			||||||
 | 
					- Each address must be a complete endpoint 
 | 
				
			||||||
 | 
					> `https://xxxx/yyy`
 | 
				
			||||||
 | 
					- Multiple addresses are connected by ', '
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEFAULT_INPUT_TEMPLATE` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `STABILITY_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Stability API key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `STABILITY_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Customize Stability API url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ENABLE_MCP` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Enable MCP(Model Context Protocol)Feature
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `PPIO_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PPIO API Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `PPIO_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PPIO API URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NodeJS >= 18, Docker >= 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -173,6 +382,9 @@ Before starting development, you must create a new `.env.local` file at project
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
OPENAI_API_KEY=<your api key here>
 | 
					OPENAI_API_KEY=<your api key here>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# if you are not able to access openai service, use this BASE_URL
 | 
				
			||||||
 | 
					BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Local Development
 | 
					### Local Development
 | 
				
			||||||
@@ -187,7 +399,6 @@ yarn dev
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Deployment
 | 
					## Deployment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Docker (Recommended)
 | 
					### Docker (Recommended)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -195,8 +406,8 @@ yarn dev
 | 
				
			|||||||
docker pull yidadaa/chatgpt-next-web
 | 
					docker pull yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					
 | 
				
			||||||
docker run -d -p 3000:3000 \
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
   -e OPENAI_API_KEY="sk-xxxx" \
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
   -e CODE="your-password" \
 | 
					   -e CODE=your-password \
 | 
				
			||||||
   yidadaa/chatgpt-next-web
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -204,9 +415,25 @@ You can start service behind a proxy:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
docker run -d -p 3000:3000 \
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
   -e OPENAI_API_KEY="sk-xxxx" \
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
   -e CODE="your-password" \
 | 
					   -e CODE=your-password \
 | 
				
			||||||
   -e PROXY_URL="http://localhost:7890" \
 | 
					   -e PROXY_URL=http://localhost:7890 \
 | 
				
			||||||
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If your proxy needs password, use:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```shell
 | 
				
			||||||
 | 
					-e PROXY_URL="http://127.0.0.1:7890 user pass"
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If enable MCP, use:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
 | 
					   -e CODE=your-password \
 | 
				
			||||||
 | 
					   -e ENABLE_MCP=true \
 | 
				
			||||||
   yidadaa/chatgpt-next-web
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -216,11 +443,25 @@ docker run -d -p 3000:3000 \
 | 
				
			|||||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
 | 
					bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Screenshots
 | 
					## Synchronizing Chat Records (UpStash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					| [简体中文](./docs/synchronise-chat-logs-cn.md) | [English](./docs/synchronise-chat-logs-en.md) | [Italiano](./docs/synchronise-chat-logs-es.md) | [日本語](./docs/synchronise-chat-logs-ja.md) | [한국어](./docs/synchronise-chat-logs-ko.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					## Documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Please go to the [docs][./docs] directory for more documentation instructions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Deploy with cloudflare (Deprecated)](./docs/cloudflare-pages-en.md)
 | 
				
			||||||
 | 
					- [Frequent Ask Questions](./docs/faq-en.md)
 | 
				
			||||||
 | 
					- [How to add a new translation](./docs/translation.md)
 | 
				
			||||||
 | 
					- [How to use Vercel (No English)](./docs/vercel-cn.md)
 | 
				
			||||||
 | 
					- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Translation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you want to add a new translation, read this [document](./docs/translation.md).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Donation
 | 
					## Donation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -228,32 +469,14 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Special Thanks
 | 
					## Special Thanks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Sponsor
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> 仅列出捐赠金额 >= 100RMB 的用户。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[@mushan0x0](https://github.com/mushan0x0)
 | 
					 | 
				
			||||||
[@ClarenceDan](https://github.com/ClarenceDan)
 | 
					 | 
				
			||||||
[@zhangjia](https://github.com/zhangjia)
 | 
					 | 
				
			||||||
[@hoochanlon](https://github.com/hoochanlon)
 | 
					 | 
				
			||||||
[@relativequantum](https://github.com/relativequantum)
 | 
					 | 
				
			||||||
[@desenmeng](https://github.com/desenmeng)
 | 
					 | 
				
			||||||
[@webees](https://github.com/webees)
 | 
					 | 
				
			||||||
[@chazzhou](https://github.com/chazzhou)
 | 
					 | 
				
			||||||
[@hauy](https://github.com/hauy)
 | 
					 | 
				
			||||||
[@Corwin006](https://github.com/Corwin006)
 | 
					 | 
				
			||||||
[@yankunsong](https://github.com/yankunsong)
 | 
					 | 
				
			||||||
[@ypwhs](https://github.com/ypwhs)
 | 
					 | 
				
			||||||
[@fxxxchao](https://github.com/fxxxchao)
 | 
					 | 
				
			||||||
[@hotic](https://github.com/hotic)
 | 
					 | 
				
			||||||
[@WingCH](https://github.com/WingCH)
 | 
					 | 
				
			||||||
[@jtung4](https://github.com/jtung4)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Contributor
 | 
					### Contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
 | 
					<a href="https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/graphs/contributors">
 | 
				
			||||||
 | 
					  <img src="https://contrib.rocks/image?repo=ChatGPTNextWeb/ChatGPT-Next-Web" />
 | 
				
			||||||
 | 
					</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## LICENSE
 | 
					## LICENSE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
 | 
					[MIT](https://opensource.org/license/mit/)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										269
									
								
								README_CN.md
									
									
									
									
									
								
							
							
						
						
									
										269
									
								
								README_CN.md
									
									
									
									
									
								
							@@ -1,28 +1,49 @@
 | 
				
			|||||||
<div align="center">
 | 
					<div align="center">
 | 
				
			||||||
<img src="./docs/images/icon.svg" alt="预览"/>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h1 align="center">ChatGPT Next Web</h1>
 | 
					<a href='#企业版'>
 | 
				
			||||||
 | 
					  <img src="./docs/images/ent.svg" alt="icon"/>
 | 
				
			||||||
 | 
					</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
一键免费部署你的私人 ChatGPT 网页应用。
 | 
					<h1 align="center">NextChat</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donate](#捐赠-donate-usdt)
 | 
					一键免费部署你的私人 ChatGPT 网页应用,支持 Claude, GPT4 & Gemini Pro 模型。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
 | 
					[NextChatAI](https://nextchat.club?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 企业版
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					满足您公司私有化部署和定制需求
 | 
				
			||||||
 | 
					- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
 | 
				
			||||||
 | 
					- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
 | 
				
			||||||
 | 
					- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
 | 
				
			||||||
 | 
					- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
 | 
				
			||||||
 | 
					- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
 | 
				
			||||||
 | 
					- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
 | 
				
			||||||
 | 
					- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					企业版咨询: **business@nextchat.dev**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<img width="300" src="https://github.com/user-attachments/assets/bb29a11d-ff75-48a8-b1f8-d2d7238cf987">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 开始使用
 | 
					## 开始使用
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
					1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
				
			||||||
2. 点击右侧按钮开始部署:
 | 
					2. 点击右侧按钮开始部署:
 | 
				
			||||||
   [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE;
 | 
					   [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE;
 | 
				
			||||||
3. 部署完毕后,即可开始使用;
 | 
					3. 部署完毕后,即可开始使用;
 | 
				
			||||||
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
 | 
					4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div align="center">
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 保持更新
 | 
					## 保持更新
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
 | 
					如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
 | 
				
			||||||
@@ -33,7 +54,8 @@
 | 
				
			|||||||
- 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。
 | 
					- 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 打开自动更新
 | 
					### 打开自动更新
 | 
				
			||||||
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
 | 
					
 | 
				
			||||||
 | 
					> 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
 | 
					当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -63,11 +85,11 @@ code1,code2,code3
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## 环境变量
 | 
					## 环境变量
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> 本项目大多数配置项都通过环境变量来设置。
 | 
					> 本项目大多数配置项都通过环境变量来设置,教程:[如何修改 Vercel 环境变量](./docs/vercel-cn.md)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `OPENAI_API_KEY` (必填项)
 | 
					### `OPENAI_API_KEY` (必填项)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
OpanAI 密钥,你在 openai 账户页面申请的 api key。
 | 
					OpenAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `CODE` (可选)
 | 
					### `CODE` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -89,9 +111,179 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
指定 OpenAI 中的组织 ID。
 | 
					指定 OpenAI 中的组织 ID。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 开发
 | 
					### `AZURE_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。
 | 
					> 形如:https://{azure-resource-url}/openai
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure 部署地址。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `AZURE_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure 密钥。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `AZURE_API_VERSION` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `GOOGLE_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Google Gemini Pro 密钥.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `GOOGLE_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Google Gemini Pro Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					anthropic claude Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_API_VERSION` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					anthropic claude Api version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					anthropic claude Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_SECRET_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu Secret Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BYTEDANCE_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ByteDance Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BYTEDANCE_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ByteDance Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ALIBABA_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					阿里云(千问)Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ALIBABA_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					阿里云(千问)Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					讯飞星火Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					讯飞星火Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_API_SECRET` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					讯飞星火Api Secret.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CHATGLM_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ChatGLM Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CHATGLM_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ChatGLM Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEEPSEEK_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DeepSeek Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEEPSEEK_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DeepSeek Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `HIDE_USER_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DISABLE_GPT4` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ENABLE_BALANCE_QUERY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如果你想启用余额查询功能,将此环境变量设置为 1 即可。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DISABLE_FAST_LINK` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `WHITE_WEBDAV_ENDPOINTS` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
 | 
				
			||||||
 | 
					- 每一个地址必须是一个完整的 endpoint
 | 
				
			||||||
 | 
					> `https://xxxx/xxx`
 | 
				
			||||||
 | 
					- 多个地址以`,`相连
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CUSTOM_MODELS` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。
 | 
				
			||||||
 | 
					> 如果你想先禁用所有模型,再启用指定模型,可以使用 `-all,+gpt-3.5-turbo`,则表示仅启用 `gpt-3.5-turbo`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
 | 
				
			||||||
 | 
					> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
 | 
				
			||||||
 | 
					> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
 | 
				
			||||||
 | 
					> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEFAULT_MODEL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					更改默认模型
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `VISION_MODELS` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 默认值:空
 | 
				
			||||||
 | 
					> 示例:`gpt-4-vision,claude-3-opus,my-custom-model` 表示为这些模型添加视觉能力,作为对默认模式匹配的补充(默认会检测包含"vision"、"claude-3"、"gemini-1.5"等关键词的模型)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					在默认模式匹配之外,添加更多具有视觉能力的模型。多个模型用逗号分隔。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEFAULT_INPUT_TEMPLATE` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `STABILITY_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Stability API密钥
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `STABILITY_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					自定义的Stability API请求地址
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ENABLE_MCP` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					启用MCP(Model Context Protocol)功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `PPIO_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PPIO API Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `PPIO_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PPIO API URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 开发
 | 
				
			||||||
 | 
					
 | 
				
			||||||
点击下方按钮,开始二次开发:
 | 
					点击下方按钮,开始二次开发:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -101,27 +293,34 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
OPENAI_API_KEY=<your api key here>
 | 
					OPENAI_API_KEY=<your api key here>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
 | 
				
			||||||
 | 
					BASE_URL=https://b.nextweb.fun/api/proxy
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 本地开发
 | 
					### 本地开发
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT;
 | 
					1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT;
 | 
				
			||||||
2. 执行 `yarn install && yarn dev` 即可。⚠️注意:此命令仅用于本地开发,不要用于部署!
 | 
					2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
 | 
				
			||||||
3. 如果你想本地部署,请使用 `yarn install && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
 | 
					3. 如果你想本地部署,请使用 `yarn install && yarn build && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 部署
 | 
					## 部署
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 宝塔面板部署
 | 
				
			||||||
 | 
					> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 容器部署 (推荐)
 | 
					### 容器部署 (推荐)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
 | 
					> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> ⚠️注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
 | 
					> ⚠️ 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
docker pull yidadaa/chatgpt-next-web
 | 
					docker pull yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					
 | 
				
			||||||
docker run -d -p 3000:3000 \
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
   -e OPENAI_API_KEY="sk-xxxx" \
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
   -e CODE="页面访问密码" \
 | 
					   -e CODE=页面访问密码 \
 | 
				
			||||||
   yidadaa/chatgpt-next-web
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -129,13 +328,29 @@ docker run -d -p 3000:3000 \
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
docker run -d -p 3000:3000 \
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
   -e OPENAI_API_KEY="sk-xxxx" \
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
   -e CODE="页面访问密码" \
 | 
					   -e CODE=页面访问密码 \
 | 
				
			||||||
   --net=host \
 | 
					   --net=host \
 | 
				
			||||||
   -e PROXY_URL="http://127.0.0.1:7890" \
 | 
					   -e PROXY_URL=http://127.0.0.1:7890 \
 | 
				
			||||||
   yidadaa/chatgpt-next-web
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如需启用 MCP 功能,可以使用:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```shell
 | 
				
			||||||
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
 | 
					   -e CODE=页面访问密码 \
 | 
				
			||||||
 | 
					   -e ENABLE_MCP=true \
 | 
				
			||||||
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如果你的本地代理需要账号密码,可以使用:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```shell
 | 
				
			||||||
 | 
					-e PROXY_URL="http://127.0.0.1:7890 user password"
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。
 | 
					如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 本地部署
 | 
					### 本地部署
 | 
				
			||||||
@@ -146,7 +361,7 @@ docker run -d -p 3000:3000 \
 | 
				
			|||||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
 | 
					bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
⚠️注意:如果你安装过程中遇到了问题,请使用 docker 部署。
 | 
					⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 鸣谢
 | 
					## 鸣谢
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -158,8 +373,10 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
 | 
					[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 相关项目
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [one-api](https://github.com/songquanpeng/one-api): 一站式大模型额度管理平台,支持市面上所有主流大语言模型
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 开源协议
 | 
					## 开源协议
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> 反对 996,从我开始。
 | 
					[MIT](https://opensource.org/license/mit/)
 | 
				
			||||||
 | 
					 | 
				
			||||||
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										317
									
								
								README_JA.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								README_JA.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,317 @@
 | 
				
			|||||||
 | 
					<div align="center">
 | 
				
			||||||
 | 
					<img src="./docs/images/ent.svg" alt="プレビュー"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h1 align="center">NextChat</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[NextChatAI](https://nextchat.club?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 企業版
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					あなたの会社のプライベートデプロイとカスタマイズのニーズに応える
 | 
				
			||||||
 | 
					- **ブランドカスタマイズ**:企業向けに特別に設計された VI/UI、企業ブランドイメージとシームレスにマッチ
 | 
				
			||||||
 | 
					- **リソース統合**:企業管理者が数十種類のAIリソースを統一管理、チームメンバーはすぐに使用可能
 | 
				
			||||||
 | 
					- **権限管理**:メンバーの権限、リソースの権限、ナレッジベースの権限を明確にし、企業レベルのAdmin Panelで統一管理
 | 
				
			||||||
 | 
					- **知識の統合**:企業内部のナレッジベースとAI機能を結びつけ、汎用AIよりも企業自身の業務ニーズに近づける
 | 
				
			||||||
 | 
					- **セキュリティ監査**:機密質問を自動的にブロックし、すべての履歴対話を追跡可能にし、AIも企業の情報セキュリティ基準に従わせる
 | 
				
			||||||
 | 
					- **プライベートデプロイ**:企業レベルのプライベートデプロイ、主要なプライベートクラウドデプロイをサポートし、データのセキュリティとプライバシーを保護
 | 
				
			||||||
 | 
					- **継続的な更新**:マルチモーダル、エージェントなどの最先端機能を継続的に更新し、常に最新であり続ける
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					企業版のお問い合わせ: **business@nextchat.dev**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 始めに
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. [OpenAI API Key](https://platform.openai.com/account/api-keys)を準備する;
 | 
				
			||||||
 | 
					2. 右側のボタンをクリックしてデプロイを開始:
 | 
				
			||||||
 | 
					   [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) 、GitHubアカウントで直接ログインし、環境変数ページにAPI Keyと[ページアクセスパスワード](#設定ページアクセスパスワード) CODEを入力してください;
 | 
				
			||||||
 | 
					3. デプロイが完了したら、すぐに使用を開始できます;
 | 
				
			||||||
 | 
					4. (オプション)[カスタムドメインをバインド](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercelが割り当てたドメインDNSは一部の地域で汚染されているため、カスタムドメインをバインドすると直接接続できます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div align="center">
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 更新を維持する
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					もし上記の手順に従ってワンクリックでプロジェクトをデプロイした場合、「更新があります」というメッセージが常に表示されることがあります。これは、Vercel がデフォルトで新しいプロジェクトを作成するためで、本プロジェクトを fork していないことが原因です。そのため、正しく更新を検出できません。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					以下の手順に従って再デプロイすることをお勧めします:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- 元のリポジトリを削除する
 | 
				
			||||||
 | 
					- ページ右上の fork ボタンを使って、本プロジェクトを fork する
 | 
				
			||||||
 | 
					- Vercel で再度選択してデプロイする、[詳細な手順はこちらを参照してください](./docs/vercel-ja.md)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 自動更新を開く
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 手動でコードを更新する
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					手動で即座に更新したい場合は、[GitHub のドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)を参照して、fork したプロジェクトを上流のコードと同期する方法を確認してください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					このプロジェクトをスターまたはウォッチしたり、作者をフォローすることで、新機能の更新通知をすぐに受け取ることができます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## ページアクセスパスワードを設定する
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> パスワードを設定すると、ユーザーは設定ページでアクセスコードを手動で入力しない限り、通常のチャットができず、未承認の状態であることを示すメッセージが表示されます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> **警告**:パスワードの桁数は十分に長く設定してください。7桁以上が望ましいです。さもないと、[ブルートフォース攻撃を受ける可能性があります](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					このプロジェクトは限られた権限管理機能を提供しています。Vercel プロジェクトのコントロールパネルで、環境変数ページに `CODE` という名前の環境変数を追加し、値をカンマで区切ったカスタムパスワードに設定してください:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					code1,code2,code3
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					この環境変数を追加または変更した後、**プロジェクトを再デプロイ**して変更を有効にしてください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 環境変数
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 本プロジェクトのほとんどの設定は環境変数で行います。チュートリアル:[Vercel の環境変数を変更する方法](./docs/vercel-ja.md)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `OPENAI_API_KEY` (必須)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OpenAI の API キー。OpenAI アカウントページで申請したキーをカンマで区切って複数設定できます。これにより、ランダムにキーが選択されます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CODE` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					アクセスパスワード。カンマで区切って複数設定可能。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**警告**:この項目を設定しないと、誰でもデプロイしたウェブサイトを利用でき、トークンが急速に消耗する可能性があるため、設定をお勧めします。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BASE_URL` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> デフォルト: `https://api.openai.com`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 例: `http://your-openai-proxy.com`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OpenAI API のプロキシ URL。手動で OpenAI API のプロキシを設定している場合はこのオプションを設定してください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> SSL 証明書の問題がある場合は、`BASE_URL` のプロトコルを http に設定してください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `OPENAI_ORG_ID` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OpenAI の組織 ID を指定します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `AZURE_URL` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 形式: https://{azure-resource-url}/openai/deployments/{deploy-name}
 | 
				
			||||||
 | 
					> `CUSTOM_MODELS` で `displayName` 形式で {deploy-name} を設定した場合、`AZURE_URL` から {deploy-name} を省略できます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure のデプロイ URL。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `AZURE_API_KEY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure の API キー。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `AZURE_API_VERSION` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure API バージョン。[Azure ドキュメント](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)で確認できます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `GOOGLE_API_KEY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Google Gemini Pro API キー。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `GOOGLE_URL` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Google Gemini Pro API の URL。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_API_KEY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Anthropic Claude API キー。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_API_VERSION` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Anthropic Claude API バージョン。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ANTHROPIC_URL` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Anthropic Claude API の URL。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_API_KEY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu API キー。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_SECRET_KEY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu シークレットキー。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BAIDU_URL` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Baidu API の URL。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BYTEDANCE_API_KEY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ByteDance API キー。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `BYTEDANCE_URL` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ByteDance API の URL。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ALIBABA_API_KEY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					アリババ(千问)API キー。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ALIBABA_URL` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					アリババ(千问)API の URL。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `HIDE_USER_API_KEY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ユーザーが API キーを入力できないようにしたい場合は、この環境変数を 1 に設定します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DISABLE_GPT4` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ユーザーが GPT-4 を使用できないようにしたい場合は、この環境変数を 1 に設定します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ENABLE_BALANCE_QUERY` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					バランスクエリ機能を有効にしたい場合は、この環境変数を 1 に設定します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DISABLE_FAST_LINK` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `WHITE_WEBDAV_ENDPOINTS` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
 | 
				
			||||||
 | 
					- 各アドレスは完全なエンドポイントでなければなりません。
 | 
				
			||||||
 | 
					> `https://xxxx/xxx`
 | 
				
			||||||
 | 
					- 複数のアドレスは `,` で接続します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CUSTOM_MODELS` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` は `qwen-7b-chat` と `glm-6b` をモデルリストに追加し、`gpt-3.5-turbo` を削除し、`gpt-4-1106-preview` のモデル名を `gpt-4-turbo` として表示します。
 | 
				
			||||||
 | 
					> すべてのモデルを無効にし、特定のモデルを有効にしたい場合は、`-all,+gpt-3.5-turbo` を使用します。これは `gpt-3.5-turbo` のみを有効にすることを意味します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
 | 
				
			||||||
 | 
					> 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
 | 
				
			||||||
 | 
					> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEFAULT_MODEL` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					デフォルトのモデルを変更します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `VISION_MODELS` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> デフォルト:空
 | 
				
			||||||
 | 
					> 例:`gpt-4-vision,claude-3-opus,my-custom-model` は、これらのモデルにビジョン機能を追加します。これはデフォルトのパターンマッチング("vision"、"claude-3"、"gemini-1.5"などのキーワードを含むモデルを検出)に加えて適用されます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					デフォルトのパターンマッチングに加えて、追加のモデルにビジョン機能を付与します。複数のモデルはカンマで区切ります。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEFAULT_INPUT_TEMPLATE` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 開発
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					下のボタンをクリックして二次開発を開始してください:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					コードを書く前に、プロジェクトのルートディレクトリに `.env.local` ファイルを新規作成し、環境変数を記入します:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					OPENAI_API_KEY=<your api key here>
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### ローカル開発
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Node.js 18 と Yarn をインストールします。具体的な方法は ChatGPT にお尋ねください。
 | 
				
			||||||
 | 
					2. `yarn install && yarn dev` を実行します。⚠️ 注意:このコマンドはローカル開発用であり、デプロイには使用しないでください。
 | 
				
			||||||
 | 
					3. ローカルでデプロイしたい場合は、`yarn install && yarn build && yarn start` コマンドを使用してください。プロセスを守るために pm2 を使用することもできます。詳細は ChatGPT にお尋ねください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## デプロイ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### コンテナデプロイ(推奨)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Docker バージョンは 20 以上が必要です。それ以下だとイメージが見つからないというエラーが出ます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> ⚠️ 注意:Docker バージョンは最新バージョンより 1~2 日遅れることが多いため、デプロイ後に「更新があります」の通知が出続けることがありますが、正常です。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```shell
 | 
				
			||||||
 | 
					docker pull yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
 | 
					   -e CODE=ページアクセスパスワード \
 | 
				
			||||||
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					プロキシを指定することもできます:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```shell
 | 
				
			||||||
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
 | 
					   -e CODE=ページアクセスパスワード \
 | 
				
			||||||
 | 
					   --net=host \
 | 
				
			||||||
 | 
					   -e PROXY_URL=http://127.0.0.1:7890 \
 | 
				
			||||||
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ローカルプロキシがアカウントとパスワードを必要とする場合は、以下を使用できます:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```shell
 | 
				
			||||||
 | 
					-e PROXY_URL="http://127.0.0.1:7890 user password"
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					他の環境変数を指定する必要がある場合は、上記のコマンドに `-e 環境変数=環境変数値` を追加して指定してください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### ローカルデプロイ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					コンソールで以下のコマンドを実行します:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```shell
 | 
				
			||||||
 | 
					bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					⚠️ 注意:インストール中に問題が発生した場合は、Docker を使用してデプロイしてください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 謝辞
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 寄付者
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 英語版をご覧ください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 貢献者
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[プロジェクトの貢献者リストはこちら](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 関連プロジェクト
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [one-api](https://github.com/songquanpeng/one-api): 一つのプラットフォームで大規模モデルのクォータ管理を提供し、市場に出回っているすべての主要な大規模言語モデルをサポートします。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## オープンソースライセンス
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MIT](https://opensource.org/license/mit/)
 | 
				
			||||||
							
								
								
									
										85
									
								
								app/api/[provider]/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								app/api/[provider]/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import { ApiPath } from "@/app/constant";
 | 
				
			||||||
 | 
					import { NextRequest } from "next/server";
 | 
				
			||||||
 | 
					import { handle as openaiHandler } from "../../openai";
 | 
				
			||||||
 | 
					import { handle as azureHandler } from "../../azure";
 | 
				
			||||||
 | 
					import { handle as googleHandler } from "../../google";
 | 
				
			||||||
 | 
					import { handle as anthropicHandler } from "../../anthropic";
 | 
				
			||||||
 | 
					import { handle as baiduHandler } from "../../baidu";
 | 
				
			||||||
 | 
					import { handle as bytedanceHandler } from "../../bytedance";
 | 
				
			||||||
 | 
					import { handle as alibabaHandler } from "../../alibaba";
 | 
				
			||||||
 | 
					import { handle as moonshotHandler } from "../../moonshot";
 | 
				
			||||||
 | 
					import { handle as stabilityHandler } from "../../stability";
 | 
				
			||||||
 | 
					import { handle as iflytekHandler } from "../../iflytek";
 | 
				
			||||||
 | 
					import { handle as deepseekHandler } from "../../deepseek";
 | 
				
			||||||
 | 
					import { handle as siliconflowHandler } from "../../siliconflow";
 | 
				
			||||||
 | 
					import { handle as ppioHandler } from "../../ppio";
 | 
				
			||||||
 | 
					import { handle as xaiHandler } from "../../xai";
 | 
				
			||||||
 | 
					import { handle as chatglmHandler } from "../../glm";
 | 
				
			||||||
 | 
					import { handle as proxyHandler } from "../../proxy";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { provider: string; path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const apiPath = `/api/${params.provider}`;
 | 
				
			||||||
 | 
					  console.log(`[${params.provider} Route] params `, params);
 | 
				
			||||||
 | 
					  switch (apiPath) {
 | 
				
			||||||
 | 
					    case ApiPath.Azure:
 | 
				
			||||||
 | 
					      return azureHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Google:
 | 
				
			||||||
 | 
					      return googleHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Anthropic:
 | 
				
			||||||
 | 
					      return anthropicHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Baidu:
 | 
				
			||||||
 | 
					      return baiduHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.ByteDance:
 | 
				
			||||||
 | 
					      return bytedanceHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Alibaba:
 | 
				
			||||||
 | 
					      return alibabaHandler(req, { params });
 | 
				
			||||||
 | 
					    // case ApiPath.Tencent: using "/api/tencent"
 | 
				
			||||||
 | 
					    case ApiPath.Moonshot:
 | 
				
			||||||
 | 
					      return moonshotHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Stability:
 | 
				
			||||||
 | 
					      return stabilityHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Iflytek:
 | 
				
			||||||
 | 
					      return iflytekHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.DeepSeek:
 | 
				
			||||||
 | 
					      return deepseekHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.XAI:
 | 
				
			||||||
 | 
					      return xaiHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.ChatGLM:
 | 
				
			||||||
 | 
					      return chatglmHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.SiliconFlow:
 | 
				
			||||||
 | 
					      return siliconflowHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.PPIO:
 | 
				
			||||||
 | 
					      return ppioHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.OpenAI:
 | 
				
			||||||
 | 
					      return openaiHandler(req, { params });
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return proxyHandler(req, { params });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
 | 
					export const preferredRegion = [
 | 
				
			||||||
 | 
					  "arn1",
 | 
				
			||||||
 | 
					  "bom1",
 | 
				
			||||||
 | 
					  "cdg1",
 | 
				
			||||||
 | 
					  "cle1",
 | 
				
			||||||
 | 
					  "cpt1",
 | 
				
			||||||
 | 
					  "dub1",
 | 
				
			||||||
 | 
					  "fra1",
 | 
				
			||||||
 | 
					  "gru1",
 | 
				
			||||||
 | 
					  "hnd1",
 | 
				
			||||||
 | 
					  "iad1",
 | 
				
			||||||
 | 
					  "icn1",
 | 
				
			||||||
 | 
					  "kix1",
 | 
				
			||||||
 | 
					  "lhr1",
 | 
				
			||||||
 | 
					  "pdx1",
 | 
				
			||||||
 | 
					  "sfo1",
 | 
				
			||||||
 | 
					  "sin1",
 | 
				
			||||||
 | 
					  "syd1",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
							
								
								
									
										129
									
								
								app/api/alibaba.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/api/alibaba.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ALIBABA_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Alibaba Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Qwen);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Alibaba] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					      "X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.Alibaba as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[Alibaba] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										170
									
								
								app/api/anthropic.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								app/api/anthropic.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ANTHROPIC_BASE_URL,
 | 
				
			||||||
 | 
					  Anthropic,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "./auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Anthropic Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const subpath = params.path.join("/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!ALLOWD_PATH.has(subpath)) {
 | 
				
			||||||
 | 
					    console.log("[Anthropic Route] forbidden path ", subpath);
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "you are not allowed to request " + subpath,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 403,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Claude);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Anthropic] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let authHeaderName = "x-api-key";
 | 
				
			||||||
 | 
					  let authValue =
 | 
				
			||||||
 | 
					    req.headers.get(authHeaderName) ||
 | 
				
			||||||
 | 
					    req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
 | 
				
			||||||
 | 
					    serverConfig.anthropicApiKey ||
 | 
				
			||||||
 | 
					    "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl =
 | 
				
			||||||
 | 
					    serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // try rebuild url, when using cloudflare ai gateway in server
 | 
				
			||||||
 | 
					  const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}${path}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      "Cache-Control": "no-store",
 | 
				
			||||||
 | 
					      "anthropic-dangerous-direct-browser-access": "true",
 | 
				
			||||||
 | 
					      [authHeaderName]: authValue,
 | 
				
			||||||
 | 
					      "anthropic-version":
 | 
				
			||||||
 | 
					        req.headers.get("anthropic-version") ||
 | 
				
			||||||
 | 
					        serverConfig.anthropicApiVersion ||
 | 
				
			||||||
 | 
					        Anthropic.Vision,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.Anthropic as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[Anthropic] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // console.log("[Anthropic request]", fetchOptions.headers, req.method);
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // console.log(
 | 
				
			||||||
 | 
					    //   "[Anthropic response]",
 | 
				
			||||||
 | 
					    //   res.status,
 | 
				
			||||||
 | 
					    //   "   ",
 | 
				
			||||||
 | 
					    //   res.headers,
 | 
				
			||||||
 | 
					    //   res.url,
 | 
				
			||||||
 | 
					    // );
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										73
									
								
								app/api/artifacts/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								app/api/artifacts/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					import md5 from "spark-md5";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handle(req: NextRequest, res: NextResponse) {
 | 
				
			||||||
 | 
					  const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					  const storeUrl = () =>
 | 
				
			||||||
 | 
					    `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
 | 
				
			||||||
 | 
					  const storeHeaders = () => ({
 | 
				
			||||||
 | 
					    Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (req.method === "POST") {
 | 
				
			||||||
 | 
					    const clonedBody = await req.text();
 | 
				
			||||||
 | 
					    const hashedCode = md5.hash(clonedBody).trim();
 | 
				
			||||||
 | 
					    const body: {
 | 
				
			||||||
 | 
					      key: string;
 | 
				
			||||||
 | 
					      value: string;
 | 
				
			||||||
 | 
					      expiration_ttl?: number;
 | 
				
			||||||
 | 
					    } = {
 | 
				
			||||||
 | 
					      key: hashedCode,
 | 
				
			||||||
 | 
					      value: clonedBody,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
 | 
				
			||||||
 | 
					      if (ttl > 60) {
 | 
				
			||||||
 | 
					        body["expiration_ttl"] = ttl;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const res = await fetch(`${storeUrl()}/bulk`, {
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        ...storeHeaders(),
 | 
				
			||||||
 | 
					        "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      method: "PUT",
 | 
				
			||||||
 | 
					      body: JSON.stringify([body]),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const result = await res.json();
 | 
				
			||||||
 | 
					    console.log("save data", result);
 | 
				
			||||||
 | 
					    if (result?.success) {
 | 
				
			||||||
 | 
					      return NextResponse.json(
 | 
				
			||||||
 | 
					        { code: 0, id: hashedCode, result },
 | 
				
			||||||
 | 
					        { status: res.status },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      { error: true, msg: "Save data error" },
 | 
				
			||||||
 | 
					      { status: 400 },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (req.method === "GET") {
 | 
				
			||||||
 | 
					    const id = req?.nextUrl?.searchParams?.get("id");
 | 
				
			||||||
 | 
					    const res = await fetch(`${storeUrl()}/values/${id}`, {
 | 
				
			||||||
 | 
					      headers: storeHeaders(),
 | 
				
			||||||
 | 
					      method: "GET",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: res.headers,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return NextResponse.json(
 | 
				
			||||||
 | 
					    { error: true, msg: "Invalid request" },
 | 
				
			||||||
 | 
					    { status: 400 },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
							
								
								
									
										132
									
								
								app/api/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								app/api/auth.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
				
			|||||||
 | 
					import { NextRequest } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "../config/server";
 | 
				
			||||||
 | 
					import md5 from "spark-md5";
 | 
				
			||||||
 | 
					import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getIP(req: NextRequest) {
 | 
				
			||||||
 | 
					  let ip = req.ip ?? req.headers.get("x-real-ip");
 | 
				
			||||||
 | 
					  const forwardedFor = req.headers.get("x-forwarded-for");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!ip && forwardedFor) {
 | 
				
			||||||
 | 
					    ip = forwardedFor.split(",").at(0) ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return ip;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function parseApiKey(bearToken: string) {
 | 
				
			||||||
 | 
					  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
				
			||||||
 | 
					  const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
 | 
				
			||||||
 | 
					    apiKey: isApiKey ? token : "",
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function auth(req: NextRequest, modelProvider: ModelProvider) {
 | 
				
			||||||
 | 
					  const authToken = req.headers.get("Authorization") ?? "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // check if it is openai api key or user token
 | 
				
			||||||
 | 
					  const { accessCode, apiKey } = parseApiKey(authToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const hashedCode = md5.hash(accessCode ?? "").trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					  console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
 | 
				
			||||||
 | 
					  console.log("[Auth] got access code:", accessCode);
 | 
				
			||||||
 | 
					  console.log("[Auth] hashed access code:", hashedCode);
 | 
				
			||||||
 | 
					  console.log("[User IP] ", getIP(req));
 | 
				
			||||||
 | 
					  console.log("[Time] ", new Date().toLocaleString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      error: true,
 | 
				
			||||||
 | 
					      msg: !accessCode ? "empty access code" : "wrong access code",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (serverConfig.hideUserApiKey && !!apiKey) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      error: true,
 | 
				
			||||||
 | 
					      msg: "you are not allowed to access with your own api key",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // if user does not provide an api key, inject system api key
 | 
				
			||||||
 | 
					  if (!apiKey) {
 | 
				
			||||||
 | 
					    const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // const systemApiKey =
 | 
				
			||||||
 | 
					    //   modelProvider === ModelProvider.GeminiPro
 | 
				
			||||||
 | 
					    //     ? serverConfig.googleApiKey
 | 
				
			||||||
 | 
					    //     : serverConfig.isAzure
 | 
				
			||||||
 | 
					    //     ? serverConfig.azureApiKey
 | 
				
			||||||
 | 
					    //     : serverConfig.apiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let systemApiKey: string | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (modelProvider) {
 | 
				
			||||||
 | 
					      case ModelProvider.Stability:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.stabilityApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.GeminiPro:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.googleApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Claude:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.anthropicApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Doubao:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.bytedanceApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Ernie:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.baiduApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Qwen:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.alibabaApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Moonshot:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.moonshotApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Iflytek:
 | 
				
			||||||
 | 
					        systemApiKey =
 | 
				
			||||||
 | 
					          serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.DeepSeek:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.deepseekApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.XAI:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.xaiApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.ChatGLM:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.chatglmApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.SiliconFlow:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.siliconFlowApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.PPIO:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.ppioApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.GPT:
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        if (req.nextUrl.pathname.includes("azure/deployments")) {
 | 
				
			||||||
 | 
					          systemApiKey = serverConfig.azureApiKey;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          systemApiKey = serverConfig.apiKey;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (systemApiKey) {
 | 
				
			||||||
 | 
					      console.log("[Auth] use system api key");
 | 
				
			||||||
 | 
					      req.headers.set("Authorization", `Bearer ${systemApiKey}`);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      console.log("[Auth] admin did not provide an api key");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    console.log("[Auth] use user api key");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    error: false,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								app/api/azure.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/api/azure.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { ModelProvider } from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "./auth";
 | 
				
			||||||
 | 
					import { requestOpenai } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Azure Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const subpath = params.path.join("/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.GPT);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    return await requestOpenai(req);
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Azure] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										144
									
								
								app/api/baidu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								app/api/baidu.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  BAIDU_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					import { getAccessToken } from "@/app/utils/baidu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Baidu Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Ernie);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!serverConfig.baiduApiKey || !serverConfig.baiduSecretKey) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        message: `missing BAIDU_API_KEY or BAIDU_SECRET_KEY in server env vars`,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 401,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Baidu] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.baiduUrl || BAIDU_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { access_token } = await getAccessToken(
 | 
				
			||||||
 | 
					    serverConfig.baiduApiKey as string,
 | 
				
			||||||
 | 
					    serverConfig.baiduSecretKey as string,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}?access_token=${access_token}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.Baidu as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[Baidu] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										129
									
								
								app/api/bytedance.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/api/bytedance.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  BYTEDANCE_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[ByteDance Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Doubao);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[ByteDance] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.bytedanceUrl || BYTEDANCE_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.ByteDance as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[ByteDance] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,62 +0,0 @@
 | 
				
			|||||||
import { createParser } from "eventsource-parser";
 | 
					 | 
				
			||||||
import { NextRequest } from "next/server";
 | 
					 | 
				
			||||||
import { requestOpenai } from "../common";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function createStream(req: NextRequest) {
 | 
					 | 
				
			||||||
  const encoder = new TextEncoder();
 | 
					 | 
				
			||||||
  const decoder = new TextDecoder();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const res = await requestOpenai(req);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const contentType = res.headers.get("Content-Type") ?? "";
 | 
					 | 
				
			||||||
  if (!contentType.includes("stream")) {
 | 
					 | 
				
			||||||
    const content = await (
 | 
					 | 
				
			||||||
      await res.text()
 | 
					 | 
				
			||||||
    ).replace(/provided:.*. You/, "provided: ***. You");
 | 
					 | 
				
			||||||
    console.log("[Stream] error ", content);
 | 
					 | 
				
			||||||
    return "```json\n" + content + "```";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const stream = new ReadableStream({
 | 
					 | 
				
			||||||
    async start(controller) {
 | 
					 | 
				
			||||||
      function onParse(event: any) {
 | 
					 | 
				
			||||||
        if (event.type === "event") {
 | 
					 | 
				
			||||||
          const data = event.data;
 | 
					 | 
				
			||||||
          // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
 | 
					 | 
				
			||||||
          if (data === "[DONE]") {
 | 
					 | 
				
			||||||
            controller.close();
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            const json = JSON.parse(data);
 | 
					 | 
				
			||||||
            const text = json.choices[0].delta.content;
 | 
					 | 
				
			||||||
            const queue = encoder.encode(text);
 | 
					 | 
				
			||||||
            controller.enqueue(queue);
 | 
					 | 
				
			||||||
          } catch (e) {
 | 
					 | 
				
			||||||
            controller.error(e);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const parser = createParser(onParse);
 | 
					 | 
				
			||||||
      for await (const chunk of res.body as any) {
 | 
					 | 
				
			||||||
        parser.feed(decoder.decode(chunk, { stream: true }));
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  return stream;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function POST(req: NextRequest) {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const stream = await createStream(req);
 | 
					 | 
				
			||||||
    return new Response(stream);
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.error("[Chat Stream]", error);
 | 
					 | 
				
			||||||
    return new Response(
 | 
					 | 
				
			||||||
      ["```json\n", JSON.stringify(error, null, "  "), "\n```"].join(""),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
@@ -1,34 +1,186 @@
 | 
				
			|||||||
import { NextRequest } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "../config/server";
 | 
				
			||||||
 | 
					import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
 | 
				
			||||||
 | 
					import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
 | 
				
			||||||
 | 
					import { getModelProvider, isModelNotavailableInServer } from "../utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const OPENAI_URL = "api.openai.com";
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
const DEFAULT_PROTOCOL = "https";
 | 
					 | 
				
			||||||
const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
 | 
					 | 
				
			||||||
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function requestOpenai(req: NextRequest) {
 | 
					export async function requestOpenai(req: NextRequest) {
 | 
				
			||||||
  const apiKey = req.headers.get("token");
 | 
					  const controller = new AbortController();
 | 
				
			||||||
  const openaiPath = req.headers.get("path");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let baseUrl = BASE_URL;
 | 
					  const isAzure = req.nextUrl.pathname.includes("azure/deployments");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var authValue,
 | 
				
			||||||
 | 
					    authHeaderName = "";
 | 
				
			||||||
 | 
					  if (isAzure) {
 | 
				
			||||||
 | 
					    authValue =
 | 
				
			||||||
 | 
					      req.headers
 | 
				
			||||||
 | 
					        .get("Authorization")
 | 
				
			||||||
 | 
					        ?.trim()
 | 
				
			||||||
 | 
					        .replaceAll("Bearer ", "")
 | 
				
			||||||
 | 
					        .trim() ?? "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    authHeaderName = "api-key";
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    authValue = req.headers.get("Authorization") ?? "";
 | 
				
			||||||
 | 
					    authHeaderName = "Authorization";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl =
 | 
				
			||||||
 | 
					    (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!baseUrl.startsWith("http")) {
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
    baseUrl = `${PROTOCOL}://${baseUrl}`;
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log("[Proxy] ", openaiPath);
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
  console.log("[Base Url]", baseUrl);
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (process.env.OPENAI_ORG_ID) {
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
    console.log("[Org ID]", process.env.OPENAI_ORG_ID);
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isAzure) {
 | 
				
			||||||
 | 
					    const azureApiVersion =
 | 
				
			||||||
 | 
					      req?.nextUrl?.searchParams?.get("api-version") ||
 | 
				
			||||||
 | 
					      serverConfig.azureApiVersion;
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.split("/deployments").shift() as string;
 | 
				
			||||||
 | 
					    path = `${req.nextUrl.pathname.replaceAll(
 | 
				
			||||||
 | 
					      "/api/azure/",
 | 
				
			||||||
 | 
					      "",
 | 
				
			||||||
 | 
					    )}?api-version=${azureApiVersion}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Forward compatibility:
 | 
				
			||||||
 | 
					    // if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL
 | 
				
			||||||
 | 
					    // then using default '{deploy-id}'
 | 
				
			||||||
 | 
					    if (serverConfig.customModels && serverConfig.azureUrl) {
 | 
				
			||||||
 | 
					      const modelName = path.split("/")[1];
 | 
				
			||||||
 | 
					      let realDeployName = "";
 | 
				
			||||||
 | 
					      serverConfig.customModels
 | 
				
			||||||
 | 
					        .split(",")
 | 
				
			||||||
 | 
					        .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
 | 
				
			||||||
 | 
					        .forEach((m) => {
 | 
				
			||||||
 | 
					          const [fullName, displayName] = m.split("=");
 | 
				
			||||||
 | 
					          const [_, providerName] = getModelProvider(fullName);
 | 
				
			||||||
 | 
					          if (providerName === "azure" && !displayName) {
 | 
				
			||||||
 | 
					            const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
 | 
				
			||||||
 | 
					              "deployments/",
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            if (deployId) {
 | 
				
			||||||
 | 
					              realDeployName = deployId;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      if (realDeployName) {
 | 
				
			||||||
 | 
					        console.log("[Replace with DeployId", realDeployName);
 | 
				
			||||||
 | 
					        path = path.replaceAll(modelName, realDeployName);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return fetch(`${baseUrl}/${openaiPath}`, {
 | 
					  const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
 | 
				
			||||||
 | 
					  console.log("fetchUrl", fetchUrl);
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
    headers: {
 | 
					    headers: {
 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
      Authorization: `Bearer ${apiKey}`,
 | 
					      "Cache-Control": "no-store",
 | 
				
			||||||
      ...(process.env.OPENAI_ORG_ID && { "OpenAI-Organization": process.env.OPENAI_ORG_ID }),
 | 
					      [authHeaderName]: authValue,
 | 
				
			||||||
 | 
					      ...(serverConfig.openaiOrgId && {
 | 
				
			||||||
 | 
					        "OpenAI-Organization": serverConfig.openaiOrgId,
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    method: req.method,
 | 
					    method: req.method,
 | 
				
			||||||
    body: req.body,
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse gpt4 request
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          [
 | 
				
			||||||
 | 
					            ServiceProvider.OpenAI,
 | 
				
			||||||
 | 
					            ServiceProvider.Azure,
 | 
				
			||||||
 | 
					            jsonBody?.model as string, // support provider-unspecified model
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error("[OpenAI] gpt4 filter", e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Extract the OpenAI-Organization header from the response
 | 
				
			||||||
 | 
					    const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if serverConfig.openaiOrgId is defined and not an empty string
 | 
				
			||||||
 | 
					    if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") {
 | 
				
			||||||
 | 
					      // If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
 | 
				
			||||||
 | 
					      console.log("[Org ID]", openaiOrganizationHeader);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      console.log("[Org ID] is not set up.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
 | 
				
			||||||
 | 
					    // Also, this is to prevent the header from being sent to the client
 | 
				
			||||||
 | 
					    if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
 | 
				
			||||||
 | 
					      newHeaders.delete("OpenAI-Organization");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
 | 
				
			||||||
 | 
					    // So if the streaming is disabled, we need to remove the content-encoding header
 | 
				
			||||||
 | 
					    // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
 | 
				
			||||||
 | 
					    // The browser will try to decode the response with brotli and fail
 | 
				
			||||||
 | 
					    newHeaders.delete("content-encoding");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,31 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { getServerSideConfig } from "../../config/server";
 | 
					import { getServerSideConfig } from "../../config/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Danger! Don not write any secret value here!
 | 
					// Danger! Do not hard code any secret value here!
 | 
				
			||||||
// 警告!不要在这里写入任何敏感信息!
 | 
					// 警告!不要在这里写入任何敏感信息!
 | 
				
			||||||
const DANGER_CONFIG = {
 | 
					const DANGER_CONFIG = {
 | 
				
			||||||
  needCode: serverConfig.needCode,
 | 
					  needCode: serverConfig.needCode,
 | 
				
			||||||
 | 
					  hideUserApiKey: serverConfig.hideUserApiKey,
 | 
				
			||||||
 | 
					  disableGPT4: serverConfig.disableGPT4,
 | 
				
			||||||
 | 
					  hideBalanceQuery: serverConfig.hideBalanceQuery,
 | 
				
			||||||
 | 
					  disableFastLink: serverConfig.disableFastLink,
 | 
				
			||||||
 | 
					  customModels: serverConfig.customModels,
 | 
				
			||||||
 | 
					  defaultModel: serverConfig.defaultModel,
 | 
				
			||||||
 | 
					  visionModels: serverConfig.visionModels,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  type DangerConfig = typeof DANGER_CONFIG;
 | 
					  type DangerConfig = typeof DANGER_CONFIG;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function POST(req: NextRequest) {
 | 
					async function handle() {
 | 
				
			||||||
  return NextResponse.json({
 | 
					  return NextResponse.json(DANGER_CONFIG);
 | 
				
			||||||
    needCode: serverConfig.needCode,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const runtime = "edge";
 | 
					export const runtime = "edge";
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								app/api/deepseek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/api/deepseek.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DEEPSEEK_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[DeepSeek Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.DeepSeek);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[DeepSeek] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.DeepSeek, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.deepseekUrl || DEEPSEEK_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.DeepSeek as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[DeepSeek] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										129
									
								
								app/api/glm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/api/glm.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  CHATGLM_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[GLM Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.ChatGLM);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[GLM] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  console.log("[Fetch Url] ", fetchUrl);
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.ChatGLM as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[GLM] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										133
									
								
								app/api/google.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								app/api/google.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "./auth";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { provider: string; path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Google Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.GeminiPro);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const bearToken =
 | 
				
			||||||
 | 
					    req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
 | 
				
			||||||
 | 
					  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const apiKey = token ? token : serverConfig.googleApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!apiKey) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        message: `missing GOOGLE_API_KEY in server env vars`,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 401,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req, apiKey);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Google] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
 | 
					export const preferredRegion = [
 | 
				
			||||||
 | 
					  "bom1",
 | 
				
			||||||
 | 
					  "cle1",
 | 
				
			||||||
 | 
					  "cpt1",
 | 
				
			||||||
 | 
					  "gru1",
 | 
				
			||||||
 | 
					  "hnd1",
 | 
				
			||||||
 | 
					  "iad1",
 | 
				
			||||||
 | 
					  "icn1",
 | 
				
			||||||
 | 
					  "kix1",
 | 
				
			||||||
 | 
					  "pdx1",
 | 
				
			||||||
 | 
					  "sfo1",
 | 
				
			||||||
 | 
					  "sin1",
 | 
				
			||||||
 | 
					  "syd1",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest, apiKey: string) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}${
 | 
				
			||||||
 | 
					    req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
 | 
				
			||||||
 | 
					  }`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Fetch Url] ", fetchUrl);
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      "Cache-Control": "no-store",
 | 
				
			||||||
 | 
					      "x-goog-api-key":
 | 
				
			||||||
 | 
					        req.headers.get("x-goog-api-key") ||
 | 
				
			||||||
 | 
					        (req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										129
									
								
								app/api/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/api/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  IFLYTEK_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					// iflytek
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Iflytek Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Iflytek);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Iflytek] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // iflytek use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.Iflytek as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[Iflytek] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										128
									
								
								app/api/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/api/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  MOONSHOT_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Moonshot Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Moonshot);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Moonshot] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.Moonshot as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[Moonshot] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										78
									
								
								app/api/openai.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								app/api/openai.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import { ModelProvider, OpenaiPath } from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "./auth";
 | 
				
			||||||
 | 
					import { requestOpenai } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getModels(remoteModelRes: OpenAIListModelResponse) {
 | 
				
			||||||
 | 
					  const config = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (config.disableGPT4) {
 | 
				
			||||||
 | 
					    remoteModelRes.data = remoteModelRes.data.filter(
 | 
				
			||||||
 | 
					      (m) =>
 | 
				
			||||||
 | 
					        !(
 | 
				
			||||||
 | 
					          m.id.startsWith("gpt-4") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("chatgpt-4o") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("o1") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("o3")
 | 
				
			||||||
 | 
					        ) || m.id.startsWith("gpt-4o-mini"),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return remoteModelRes;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[OpenAI Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const subpath = params.path.join("/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!ALLOWED_PATH.has(subpath)) {
 | 
				
			||||||
 | 
					    console.log("[OpenAI Route] forbidden path ", subpath);
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "you are not allowed to request " + subpath,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 403,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.GPT);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await requestOpenai(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // list models
 | 
				
			||||||
 | 
					    if (subpath === OpenaiPath.ListModelPath && response.status === 200) {
 | 
				
			||||||
 | 
					      const resJson = (await response.json()) as OpenAIListModelResponse;
 | 
				
			||||||
 | 
					      const availableModels = getModels(resJson);
 | 
				
			||||||
 | 
					      return NextResponse.json(availableModels, {
 | 
				
			||||||
 | 
					        status: response.status,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[OpenAI] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,33 +0,0 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					 | 
				
			||||||
import { requestOpenai } from "../common";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function makeRequest(req: NextRequest) {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const api = await requestOpenai(req);
 | 
					 | 
				
			||||||
    const res = new NextResponse(api.body);
 | 
					 | 
				
			||||||
    res.headers.set("Content-Type", "application/json");
 | 
					 | 
				
			||||||
    res.headers.set("Cache-Control", "no-cache");
 | 
					 | 
				
			||||||
    return res;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error("[OpenAI] ", req.body, e);
 | 
					 | 
				
			||||||
    return NextResponse.json(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        error: true,
 | 
					 | 
				
			||||||
        msg: JSON.stringify(e),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        status: 500,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function POST(req: NextRequest) {
 | 
					 | 
				
			||||||
  return makeRequest(req);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function GET(req: NextRequest) {
 | 
					 | 
				
			||||||
  return makeRequest(req);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
@@ -1,9 +0,0 @@
 | 
				
			|||||||
import type {
 | 
					 | 
				
			||||||
  CreateChatCompletionRequest,
 | 
					 | 
				
			||||||
  CreateChatCompletionResponse,
 | 
					 | 
				
			||||||
} from "openai";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type ChatRequest = CreateChatCompletionRequest;
 | 
					 | 
				
			||||||
export type ChatResponse = CreateChatCompletionResponse;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type Updater<T> = (updater: (value: T) => void) => void;
 | 
					 | 
				
			||||||
							
								
								
									
										128
									
								
								app/api/ppio.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/api/ppio.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  PPIO_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[PPIO Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.PPIO);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[PPIO] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.PPIO, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.ppioUrl || PPIO_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.PPIO as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[PPIO] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										89
									
								
								app/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								app/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Proxy Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // remove path params from searchParams
 | 
				
			||||||
 | 
					  req.nextUrl.searchParams.delete("path");
 | 
				
			||||||
 | 
					  req.nextUrl.searchParams.delete("provider");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const subpath = params.path.join("/");
 | 
				
			||||||
 | 
					  const fetchUrl = `${req.headers.get(
 | 
				
			||||||
 | 
					    "x-base-url",
 | 
				
			||||||
 | 
					  )}/${subpath}?${req.nextUrl.searchParams.toString()}`;
 | 
				
			||||||
 | 
					  const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
 | 
				
			||||||
 | 
					  const headers = new Headers(
 | 
				
			||||||
 | 
					    Array.from(req.headers.entries()).filter((item) => {
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        item[0].indexOf("x-") > -1 ||
 | 
				
			||||||
 | 
					        item[0].indexOf("sec-") > -1 ||
 | 
				
			||||||
 | 
					        skipHeaders.includes(item[0])
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  // if dalle3 use openai api key
 | 
				
			||||||
 | 
					    const baseUrl = req.headers.get("x-base-url");
 | 
				
			||||||
 | 
					    if (baseUrl?.includes("api.openai.com")) {
 | 
				
			||||||
 | 
					      if (!serverConfig.apiKey) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          { error: "OpenAI API key not configured" },
 | 
				
			||||||
 | 
					          { status: 500 },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers,
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
 | 
				
			||||||
 | 
					    // So if the streaming is disabled, we need to remove the content-encoding header
 | 
				
			||||||
 | 
					    // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
 | 
				
			||||||
 | 
					    // The browser will try to decode the response with brotli and fail
 | 
				
			||||||
 | 
					    newHeaders.delete("content-encoding");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										128
									
								
								app/api/siliconflow.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/api/siliconflow.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  SILICONFLOW_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[SiliconFlow Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.SiliconFlow);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[SiliconFlow] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.SiliconFlow, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.siliconFlowUrl || SILICONFLOW_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.SiliconFlow as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[SiliconFlow] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										99
									
								
								app/api/stability.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								app/api/stability.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Stability] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Stability Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Stability Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Stability);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const bearToken = req.headers.get("Authorization") ?? "";
 | 
				
			||||||
 | 
					  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const key = token ? token : serverConfig.stabilityApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!key) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        message: `missing STABILITY_API_KEY in server env vars`,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 401,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}/${path}`;
 | 
				
			||||||
 | 
					  console.log("[Stability Url] ", fetchUrl);
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
 | 
				
			||||||
 | 
					      Accept: req.headers.get("Accept") || "application/json",
 | 
				
			||||||
 | 
					      Authorization: `Bearer ${key}`,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										117
									
								
								app/api/tencent/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								app/api/tencent/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { getHeader } from "@/app/utils/tencent";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Tencent Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Hunyuan);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Tencent] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
 | 
					export const preferredRegion = [
 | 
				
			||||||
 | 
					  "arn1",
 | 
				
			||||||
 | 
					  "bom1",
 | 
				
			||||||
 | 
					  "cdg1",
 | 
				
			||||||
 | 
					  "cle1",
 | 
				
			||||||
 | 
					  "cpt1",
 | 
				
			||||||
 | 
					  "dub1",
 | 
				
			||||||
 | 
					  "fra1",
 | 
				
			||||||
 | 
					  "gru1",
 | 
				
			||||||
 | 
					  "hnd1",
 | 
				
			||||||
 | 
					  "iad1",
 | 
				
			||||||
 | 
					  "icn1",
 | 
				
			||||||
 | 
					  "kix1",
 | 
				
			||||||
 | 
					  "lhr1",
 | 
				
			||||||
 | 
					  "pdx1",
 | 
				
			||||||
 | 
					  "sfo1",
 | 
				
			||||||
 | 
					  "sin1",
 | 
				
			||||||
 | 
					  "syd1",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = baseUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const body = await req.text();
 | 
				
			||||||
 | 
					  const headers = await getHeader(
 | 
				
			||||||
 | 
					    body,
 | 
				
			||||||
 | 
					    serverConfig.tencentSecretId as string,
 | 
				
			||||||
 | 
					    serverConfig.tencentSecretKey as string,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers,
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										73
									
								
								app/api/upstash/[action]/[...key]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								app/api/upstash/[action]/[...key]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { action: string; key: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const requestUrl = new URL(req.url);
 | 
				
			||||||
 | 
					  const endpoint = requestUrl.searchParams.get("endpoint");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const [...key] = params.key;
 | 
				
			||||||
 | 
					  // only allow to request to *.upstash.io
 | 
				
			||||||
 | 
					  if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "you are not allowed to request " + params.key.join("/"),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 403,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // only allow upstash get and set method
 | 
				
			||||||
 | 
					  if (params.action !== "get" && params.action !== "set") {
 | 
				
			||||||
 | 
					    console.log("[Upstash Route] forbidden action ", params.action);
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "you are not allowed to request " + params.action,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 403,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const method = req.method;
 | 
				
			||||||
 | 
					  const shouldNotHaveBody = ["get", "head"].includes(
 | 
				
			||||||
 | 
					    method?.toLowerCase() ?? "",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      authorization: req.headers.get("authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: shouldNotHaveBody ? null : req.body,
 | 
				
			||||||
 | 
					    method,
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Upstash Proxy]", targetUrl, fetchOptions);
 | 
				
			||||||
 | 
					  const fetchResult = await fetch(targetUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Any Proxy]", targetUrl, {
 | 
				
			||||||
 | 
					    status: fetchResult.status,
 | 
				
			||||||
 | 
					    statusText: fetchResult.statusText,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return fetchResult;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const OPTIONS = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
							
								
								
									
										167
									
								
								app/api/webdav/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								app/api/webdav/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
				
			|||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const config = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mergedAllowedWebDavEndpoints = [
 | 
				
			||||||
 | 
					  ...internalAllowedWebDavEndpoints,
 | 
				
			||||||
 | 
					  ...config.allowedWebDavEndpoints,
 | 
				
			||||||
 | 
					].filter((domain) => Boolean(domain.trim()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const normalizeUrl = (url: string) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    return new URL(url);
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const folder = STORAGE_KEY;
 | 
				
			||||||
 | 
					  const fileName = `${folder}/backup.json`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const requestUrl = new URL(req.url);
 | 
				
			||||||
 | 
					  let endpoint = requestUrl.searchParams.get("endpoint");
 | 
				
			||||||
 | 
					  let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Validate the endpoint to prevent potential SSRF attacks
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    !endpoint ||
 | 
				
			||||||
 | 
					    !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
 | 
				
			||||||
 | 
					      const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
 | 
				
			||||||
 | 
					      const normalizedEndpoint = normalizeUrl(endpoint as string);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        normalizedEndpoint &&
 | 
				
			||||||
 | 
					        normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
 | 
				
			||||||
 | 
					        normalizedEndpoint.pathname.startsWith(
 | 
				
			||||||
 | 
					          normalizedAllowedEndpoint.pathname,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "Invalid endpoint",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 400,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!endpoint?.endsWith("/")) {
 | 
				
			||||||
 | 
					    endpoint += "/";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const endpointPath = params.path.join("/");
 | 
				
			||||||
 | 
					  const targetPath = `${endpoint}${endpointPath}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // only allow MKCOL, GET, PUT
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    proxy_method !== "MKCOL" &&
 | 
				
			||||||
 | 
					    proxy_method !== "GET" &&
 | 
				
			||||||
 | 
					    proxy_method !== "PUT"
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "you are not allowed to request " + targetPath,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 403,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // for MKCOL request, only allow request ${folder}
 | 
				
			||||||
 | 
					  if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "you are not allowed to request " + targetPath,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 403,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // for GET request, only allow request ending with fileName
 | 
				
			||||||
 | 
					  if (proxy_method === "GET" && !targetPath.endsWith(fileName)) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "you are not allowed to request " + targetPath,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 403,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //   for PUT request, only allow request ending with fileName
 | 
				
			||||||
 | 
					  if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        msg: "you are not allowed to request " + targetPath,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 403,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const targetUrl = targetPath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const method = proxy_method || req.method;
 | 
				
			||||||
 | 
					  const shouldNotHaveBody = ["get", "head"].includes(
 | 
				
			||||||
 | 
					    method?.toLowerCase() ?? "",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      authorization: req.headers.get("authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: shouldNotHaveBody ? null : req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    method,
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let fetchResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    fetchResult = await fetch(targetUrl, fetchOptions);
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    console.log(
 | 
				
			||||||
 | 
					      "[Any Proxy]",
 | 
				
			||||||
 | 
					      targetUrl,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        method: method,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: fetchResult?.status,
 | 
				
			||||||
 | 
					        statusText: fetchResult?.statusText,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return fetchResult;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PUT = handle;
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const OPTIONS = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
							
								
								
									
										128
									
								
								app/api/xai.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/api/xai.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  XAI_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[XAI Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.XAI);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[XAI] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.XAI as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[XAI] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										399
									
								
								app/client/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								app/client/api.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,399 @@
 | 
				
			|||||||
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ACCESS_CODE_PREFIX,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "../constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  ChatMessage,
 | 
				
			||||||
 | 
					  ModelType,
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					} from "../store";
 | 
				
			||||||
 | 
					import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
 | 
				
			||||||
 | 
					import { GeminiProApi } from "./platforms/google";
 | 
				
			||||||
 | 
					import { ClaudeApi } from "./platforms/anthropic";
 | 
				
			||||||
 | 
					import { ErnieApi } from "./platforms/baidu";
 | 
				
			||||||
 | 
					import { DoubaoApi } from "./platforms/bytedance";
 | 
				
			||||||
 | 
					import { QwenApi } from "./platforms/alibaba";
 | 
				
			||||||
 | 
					import { HunyuanApi } from "./platforms/tencent";
 | 
				
			||||||
 | 
					import { MoonshotApi } from "./platforms/moonshot";
 | 
				
			||||||
 | 
					import { SparkApi } from "./platforms/iflytek";
 | 
				
			||||||
 | 
					import { DeepSeekApi } from "./platforms/deepseek";
 | 
				
			||||||
 | 
					import { XAIApi } from "./platforms/xai";
 | 
				
			||||||
 | 
					import { ChatGLMApi } from "./platforms/glm";
 | 
				
			||||||
 | 
					import { SiliconflowApi } from "./platforms/siliconflow";
 | 
				
			||||||
 | 
					import { PPIOApi } from "./platforms/ppio";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ROLES = ["system", "user", "assistant"] as const;
 | 
				
			||||||
 | 
					export type MessageRole = (typeof ROLES)[number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
 | 
				
			||||||
 | 
					export const TTSModels = ["tts-1", "tts-1-hd"] as const;
 | 
				
			||||||
 | 
					export type ChatModel = ModelType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface MultimodalContent {
 | 
				
			||||||
 | 
					  type: "text" | "image_url";
 | 
				
			||||||
 | 
					  text?: string;
 | 
				
			||||||
 | 
					  image_url?: {
 | 
				
			||||||
 | 
					    url: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface MultimodalContentForAlibaba {
 | 
				
			||||||
 | 
					  text?: string;
 | 
				
			||||||
 | 
					  image?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RequestMessage {
 | 
				
			||||||
 | 
					  role: MessageRole;
 | 
				
			||||||
 | 
					  content: string | MultimodalContent[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LLMConfig {
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  providerName?: string;
 | 
				
			||||||
 | 
					  temperature?: number;
 | 
				
			||||||
 | 
					  top_p?: number;
 | 
				
			||||||
 | 
					  stream?: boolean;
 | 
				
			||||||
 | 
					  presence_penalty?: number;
 | 
				
			||||||
 | 
					  frequency_penalty?: number;
 | 
				
			||||||
 | 
					  size?: DalleRequestPayload["size"];
 | 
				
			||||||
 | 
					  quality?: DalleRequestPayload["quality"];
 | 
				
			||||||
 | 
					  style?: DalleRequestPayload["style"];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SpeechOptions {
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  input: string;
 | 
				
			||||||
 | 
					  voice: string;
 | 
				
			||||||
 | 
					  response_format?: string;
 | 
				
			||||||
 | 
					  speed?: number;
 | 
				
			||||||
 | 
					  onController?: (controller: AbortController) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatOptions {
 | 
				
			||||||
 | 
					  messages: RequestMessage[];
 | 
				
			||||||
 | 
					  config: LLMConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onUpdate?: (message: string, chunk: string) => void;
 | 
				
			||||||
 | 
					  onFinish: (message: string, responseRes: Response) => void;
 | 
				
			||||||
 | 
					  onError?: (err: Error) => void;
 | 
				
			||||||
 | 
					  onController?: (controller: AbortController) => void;
 | 
				
			||||||
 | 
					  onBeforeTool?: (tool: ChatMessageTool) => void;
 | 
				
			||||||
 | 
					  onAfterTool?: (tool: ChatMessageTool) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LLMUsage {
 | 
				
			||||||
 | 
					  used: number;
 | 
				
			||||||
 | 
					  total: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LLMModel {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  displayName?: string;
 | 
				
			||||||
 | 
					  available: boolean;
 | 
				
			||||||
 | 
					  provider: LLMModelProvider;
 | 
				
			||||||
 | 
					  sorted: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LLMModelProvider {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  providerName: string;
 | 
				
			||||||
 | 
					  providerType: string;
 | 
				
			||||||
 | 
					  sorted: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export abstract class LLMApi {
 | 
				
			||||||
 | 
					  abstract chat(options: ChatOptions): Promise<void>;
 | 
				
			||||||
 | 
					  abstract speech(options: SpeechOptions): Promise<ArrayBuffer>;
 | 
				
			||||||
 | 
					  abstract usage(): Promise<LLMUsage>;
 | 
				
			||||||
 | 
					  abstract models(): Promise<LLMModel[]>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProviderName = "openai" | "azure" | "claude" | "palm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Model {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  provider: ProviderName;
 | 
				
			||||||
 | 
					  ctxlen: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChatProvider {
 | 
				
			||||||
 | 
					  name: ProviderName;
 | 
				
			||||||
 | 
					  apiConfig: {
 | 
				
			||||||
 | 
					    baseUrl: string;
 | 
				
			||||||
 | 
					    apiKey: string;
 | 
				
			||||||
 | 
					    summaryModel: Model;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  models: Model[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chat: () => void;
 | 
				
			||||||
 | 
					  usage: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ClientApi {
 | 
				
			||||||
 | 
					  public llm: LLMApi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(provider: ModelProvider = ModelProvider.GPT) {
 | 
				
			||||||
 | 
					    switch (provider) {
 | 
				
			||||||
 | 
					      case ModelProvider.GeminiPro:
 | 
				
			||||||
 | 
					        this.llm = new GeminiProApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Claude:
 | 
				
			||||||
 | 
					        this.llm = new ClaudeApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Ernie:
 | 
				
			||||||
 | 
					        this.llm = new ErnieApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Doubao:
 | 
				
			||||||
 | 
					        this.llm = new DoubaoApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Qwen:
 | 
				
			||||||
 | 
					        this.llm = new QwenApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Hunyuan:
 | 
				
			||||||
 | 
					        this.llm = new HunyuanApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Moonshot:
 | 
				
			||||||
 | 
					        this.llm = new MoonshotApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Iflytek:
 | 
				
			||||||
 | 
					        this.llm = new SparkApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.DeepSeek:
 | 
				
			||||||
 | 
					        this.llm = new DeepSeekApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.XAI:
 | 
				
			||||||
 | 
					        this.llm = new XAIApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.ChatGLM:
 | 
				
			||||||
 | 
					        this.llm = new ChatGLMApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.SiliconFlow:
 | 
				
			||||||
 | 
					        this.llm = new SiliconflowApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.PPIO:
 | 
				
			||||||
 | 
					        this.llm = new PPIOApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        this.llm = new ChatGPTApi();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  config() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  prompts() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  masks() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async share(messages: ChatMessage[], avatarUrl: string | null = null) {
 | 
				
			||||||
 | 
					    const msgs = messages
 | 
				
			||||||
 | 
					      .map((m) => ({
 | 
				
			||||||
 | 
					        from: m.role === "user" ? "human" : "gpt",
 | 
				
			||||||
 | 
					        value: m.content,
 | 
				
			||||||
 | 
					      }))
 | 
				
			||||||
 | 
					      .concat([
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          from: "human",
 | 
				
			||||||
 | 
					          value:
 | 
				
			||||||
 | 
					            "Share from [NextChat]: https://github.com/Yidadaa/ChatGPT-Next-Web",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    // 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用
 | 
				
			||||||
 | 
					    // Please do not modify this message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Share]", messages, msgs);
 | 
				
			||||||
 | 
					    const clientConfig = getClientConfig();
 | 
				
			||||||
 | 
					    const proxyUrl = "/sharegpt";
 | 
				
			||||||
 | 
					    const rawUrl = "https://sharegpt.com/api/conversations";
 | 
				
			||||||
 | 
					    const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl;
 | 
				
			||||||
 | 
					    const res = await fetch(shareUrl, {
 | 
				
			||||||
 | 
					      body: JSON.stringify({
 | 
				
			||||||
 | 
					        avatarUrl,
 | 
				
			||||||
 | 
					        items: msgs,
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const resJson = await res.json();
 | 
				
			||||||
 | 
					    console.log("[Share]", resJson);
 | 
				
			||||||
 | 
					    if (resJson.id) {
 | 
				
			||||||
 | 
					      return `https://shareg.pt/${resJson.id}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getBearerToken(
 | 
				
			||||||
 | 
					  apiKey: string,
 | 
				
			||||||
 | 
					  noBearer: boolean = false,
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
					  return validString(apiKey)
 | 
				
			||||||
 | 
					    ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
 | 
				
			||||||
 | 
					    : "";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function validString(x: string): boolean {
 | 
				
			||||||
 | 
					  return x?.length > 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getHeaders(ignoreHeaders: boolean = false) {
 | 
				
			||||||
 | 
					  const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					  const chatStore = useChatStore.getState();
 | 
				
			||||||
 | 
					  let headers: Record<string, string> = {};
 | 
				
			||||||
 | 
					  if (!ignoreHeaders) {
 | 
				
			||||||
 | 
					    headers = {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Accept: "application/json",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const clientConfig = getClientConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getConfig() {
 | 
				
			||||||
 | 
					    const modelConfig = chatStore.currentSession().mask.modelConfig;
 | 
				
			||||||
 | 
					    const isGoogle = modelConfig.providerName === ServiceProvider.Google;
 | 
				
			||||||
 | 
					    const isAzure = modelConfig.providerName === ServiceProvider.Azure;
 | 
				
			||||||
 | 
					    const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
 | 
				
			||||||
 | 
					    const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
 | 
				
			||||||
 | 
					    const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
 | 
				
			||||||
 | 
					    const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
 | 
				
			||||||
 | 
					    const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
 | 
				
			||||||
 | 
					    const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
 | 
				
			||||||
 | 
					    const isDeepSeek = modelConfig.providerName === ServiceProvider.DeepSeek;
 | 
				
			||||||
 | 
					    const isXAI = modelConfig.providerName === ServiceProvider.XAI;
 | 
				
			||||||
 | 
					    const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM;
 | 
				
			||||||
 | 
					    const isSiliconFlow =
 | 
				
			||||||
 | 
					      modelConfig.providerName === ServiceProvider.SiliconFlow;
 | 
				
			||||||
 | 
					    const isPPIO = modelConfig.providerName === ServiceProvider.PPIO;
 | 
				
			||||||
 | 
					    const isEnabledAccessControl = accessStore.enabledAccessControl();
 | 
				
			||||||
 | 
					    const apiKey = isGoogle
 | 
				
			||||||
 | 
					      ? accessStore.googleApiKey
 | 
				
			||||||
 | 
					      : isAzure
 | 
				
			||||||
 | 
					      ? accessStore.azureApiKey
 | 
				
			||||||
 | 
					      : isAnthropic
 | 
				
			||||||
 | 
					      ? accessStore.anthropicApiKey
 | 
				
			||||||
 | 
					      : isByteDance
 | 
				
			||||||
 | 
					      ? accessStore.bytedanceApiKey
 | 
				
			||||||
 | 
					      : isAlibaba
 | 
				
			||||||
 | 
					      ? accessStore.alibabaApiKey
 | 
				
			||||||
 | 
					      : isMoonshot
 | 
				
			||||||
 | 
					      ? accessStore.moonshotApiKey
 | 
				
			||||||
 | 
					      : isXAI
 | 
				
			||||||
 | 
					      ? accessStore.xaiApiKey
 | 
				
			||||||
 | 
					      : isDeepSeek
 | 
				
			||||||
 | 
					      ? accessStore.deepseekApiKey
 | 
				
			||||||
 | 
					      : isChatGLM
 | 
				
			||||||
 | 
					      ? accessStore.chatglmApiKey
 | 
				
			||||||
 | 
					      : isSiliconFlow
 | 
				
			||||||
 | 
					      ? accessStore.siliconflowApiKey
 | 
				
			||||||
 | 
					      : isPPIO
 | 
				
			||||||
 | 
					      ? accessStore.ppioApiKey
 | 
				
			||||||
 | 
					      : isIflytek
 | 
				
			||||||
 | 
					      ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
 | 
				
			||||||
 | 
					        ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
 | 
				
			||||||
 | 
					        : ""
 | 
				
			||||||
 | 
					      : accessStore.openaiApiKey;
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      isGoogle,
 | 
				
			||||||
 | 
					      isAzure,
 | 
				
			||||||
 | 
					      isAnthropic,
 | 
				
			||||||
 | 
					      isBaidu,
 | 
				
			||||||
 | 
					      isByteDance,
 | 
				
			||||||
 | 
					      isAlibaba,
 | 
				
			||||||
 | 
					      isMoonshot,
 | 
				
			||||||
 | 
					      isIflytek,
 | 
				
			||||||
 | 
					      isDeepSeek,
 | 
				
			||||||
 | 
					      isXAI,
 | 
				
			||||||
 | 
					      isChatGLM,
 | 
				
			||||||
 | 
					      isSiliconFlow,
 | 
				
			||||||
 | 
					      isPPIO,
 | 
				
			||||||
 | 
					      apiKey,
 | 
				
			||||||
 | 
					      isEnabledAccessControl,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getAuthHeader(): string {
 | 
				
			||||||
 | 
					    return isAzure
 | 
				
			||||||
 | 
					      ? "api-key"
 | 
				
			||||||
 | 
					      : isAnthropic
 | 
				
			||||||
 | 
					      ? "x-api-key"
 | 
				
			||||||
 | 
					      : isGoogle
 | 
				
			||||||
 | 
					      ? "x-goog-api-key"
 | 
				
			||||||
 | 
					      : "Authorization";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    isGoogle,
 | 
				
			||||||
 | 
					    isAzure,
 | 
				
			||||||
 | 
					    isAnthropic,
 | 
				
			||||||
 | 
					    isBaidu,
 | 
				
			||||||
 | 
					    isByteDance,
 | 
				
			||||||
 | 
					    isAlibaba,
 | 
				
			||||||
 | 
					    isMoonshot,
 | 
				
			||||||
 | 
					    isIflytek,
 | 
				
			||||||
 | 
					    isDeepSeek,
 | 
				
			||||||
 | 
					    isXAI,
 | 
				
			||||||
 | 
					    isChatGLM,
 | 
				
			||||||
 | 
					    isSiliconFlow,
 | 
				
			||||||
 | 
					    isPPIO,
 | 
				
			||||||
 | 
					    apiKey,
 | 
				
			||||||
 | 
					    isEnabledAccessControl,
 | 
				
			||||||
 | 
					  } = getConfig();
 | 
				
			||||||
 | 
					  // when using baidu api in app, not set auth header
 | 
				
			||||||
 | 
					  if (isBaidu && clientConfig?.isApp) return headers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authHeader = getAuthHeader();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const bearerToken = getBearerToken(
 | 
				
			||||||
 | 
					    apiKey,
 | 
				
			||||||
 | 
					    isAzure || isAnthropic || isGoogle,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (bearerToken) {
 | 
				
			||||||
 | 
					    headers[authHeader] = bearerToken;
 | 
				
			||||||
 | 
					  } else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
 | 
				
			||||||
 | 
					    headers["Authorization"] = getBearerToken(
 | 
				
			||||||
 | 
					      ACCESS_CODE_PREFIX + accessStore.accessCode,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return headers;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getClientApi(provider: ServiceProvider): ClientApi {
 | 
				
			||||||
 | 
					  switch (provider) {
 | 
				
			||||||
 | 
					    case ServiceProvider.Google:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.GeminiPro);
 | 
				
			||||||
 | 
					    case ServiceProvider.Anthropic:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Claude);
 | 
				
			||||||
 | 
					    case ServiceProvider.Baidu:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Ernie);
 | 
				
			||||||
 | 
					    case ServiceProvider.ByteDance:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Doubao);
 | 
				
			||||||
 | 
					    case ServiceProvider.Alibaba:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Qwen);
 | 
				
			||||||
 | 
					    case ServiceProvider.Tencent:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Hunyuan);
 | 
				
			||||||
 | 
					    case ServiceProvider.Moonshot:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Moonshot);
 | 
				
			||||||
 | 
					    case ServiceProvider.Iflytek:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Iflytek);
 | 
				
			||||||
 | 
					    case ServiceProvider.DeepSeek:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.DeepSeek);
 | 
				
			||||||
 | 
					    case ServiceProvider.XAI:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.XAI);
 | 
				
			||||||
 | 
					    case ServiceProvider.ChatGLM:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.ChatGLM);
 | 
				
			||||||
 | 
					    case ServiceProvider.SiliconFlow:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.SiliconFlow);
 | 
				
			||||||
 | 
					    case ServiceProvider.PPIO:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.PPIO);
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.GPT);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								app/client/controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/client/controller.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					// To store message streaming controller
 | 
				
			||||||
 | 
					export const ChatControllerPool = {
 | 
				
			||||||
 | 
					  controllers: {} as Record<string, AbortController>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  addController(
 | 
				
			||||||
 | 
					    sessionId: string,
 | 
				
			||||||
 | 
					    messageId: string,
 | 
				
			||||||
 | 
					    controller: AbortController,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const key = this.key(sessionId, messageId);
 | 
				
			||||||
 | 
					    this.controllers[key] = controller;
 | 
				
			||||||
 | 
					    return key;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  stop(sessionId: string, messageId: string) {
 | 
				
			||||||
 | 
					    const key = this.key(sessionId, messageId);
 | 
				
			||||||
 | 
					    const controller = this.controllers[key];
 | 
				
			||||||
 | 
					    controller?.abort();
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  stopAll() {
 | 
				
			||||||
 | 
					    Object.values(this.controllers).forEach((v) => v.abort());
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  hasPending() {
 | 
				
			||||||
 | 
					    return Object.values(this.controllers).length > 0;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  remove(sessionId: string, messageId: string) {
 | 
				
			||||||
 | 
					    const key = this.key(sessionId, messageId);
 | 
				
			||||||
 | 
					    delete this.controllers[key];
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  key(sessionId: string, messageIndex: string) {
 | 
				
			||||||
 | 
					    return `${sessionId},${messageIndex}`;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										277
									
								
								app/client/platforms/alibaba.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								app/client/platforms/alibaba.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,277 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, Alibaba, ALIBABA_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  preProcessImageContentForAlibabaDashScope,
 | 
				
			||||||
 | 
					  streamWithThink,
 | 
				
			||||||
 | 
					} from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  MultimodalContentForAlibaba,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RequestInput {
 | 
				
			||||||
 | 
					  messages: {
 | 
				
			||||||
 | 
					    role: "system" | "user" | "assistant";
 | 
				
			||||||
 | 
					    content: string | MultimodalContent[];
 | 
				
			||||||
 | 
					  }[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					interface RequestParam {
 | 
				
			||||||
 | 
					  result_format: string;
 | 
				
			||||||
 | 
					  incremental_output?: boolean;
 | 
				
			||||||
 | 
					  temperature: number;
 | 
				
			||||||
 | 
					  repetition_penalty?: number;
 | 
				
			||||||
 | 
					  top_p: number;
 | 
				
			||||||
 | 
					  max_tokens?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					interface RequestPayload {
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  input: RequestInput;
 | 
				
			||||||
 | 
					  parameters: RequestParam;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class QwenApi implements LLMApi {
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.alibabaUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Alibaba)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res?.output?.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = (
 | 
				
			||||||
 | 
					        visionModel
 | 
				
			||||||
 | 
					          ? await preProcessImageContentForAlibabaDashScope(v.content)
 | 
				
			||||||
 | 
					          : v.role === "assistant"
 | 
				
			||||||
 | 
					          ? getMessageTextContentWithoutThinking(v)
 | 
				
			||||||
 | 
					          : getMessageTextContent(v)
 | 
				
			||||||
 | 
					      ) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      input: {
 | 
				
			||||||
 | 
					        messages,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      parameters: {
 | 
				
			||||||
 | 
					        result_format: "message",
 | 
				
			||||||
 | 
					        incremental_output: shouldStream,
 | 
				
			||||||
 | 
					        temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					        // max_tokens: modelConfig.max_tokens,
 | 
				
			||||||
 | 
					        top_p: modelConfig.top_p === 1 ? 0.99 : modelConfig.top_p, // qwen top_p is should be < 1
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const headers = {
 | 
				
			||||||
 | 
					        ...getHeaders(),
 | 
				
			||||||
 | 
					        "X-DashScope-SSE": shouldStream ? "enable" : "disable",
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const chatPath = this.path(Alibaba.ChatPath(modelConfig.model));
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: headers,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          headers,
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.output.choices as Array<{
 | 
				
			||||||
 | 
					              message: {
 | 
				
			||||||
 | 
					                content: string | null | MultimodalContentForAlibaba[];
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.message?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.message?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.message?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: Array.isArray(content)
 | 
				
			||||||
 | 
					                  ? content.map((item) => item.text).join(",")
 | 
				
			||||||
 | 
					                  : content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            requestPayload?.input?.messages?.splice(
 | 
				
			||||||
 | 
					              requestPayload?.input?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export { Alibaba };
 | 
				
			||||||
							
								
								
									
										415
									
								
								app/client/platforms/anthropic.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										415
									
								
								app/client/platforms/anthropic.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,415 @@
 | 
				
			|||||||
 | 
					import { Anthropic, ApiPath } from "@/app/constant";
 | 
				
			||||||
 | 
					import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { ANTHROPIC_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { getMessageTextContent, isVisionModel } from "@/app/utils";
 | 
				
			||||||
 | 
					import { preProcessImageContent, stream } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type MultiBlockContent = {
 | 
				
			||||||
 | 
					  type: "image" | "text";
 | 
				
			||||||
 | 
					  source?: {
 | 
				
			||||||
 | 
					    type: string;
 | 
				
			||||||
 | 
					    media_type: string;
 | 
				
			||||||
 | 
					    data: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  text?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AnthropicMessage = {
 | 
				
			||||||
 | 
					  role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
 | 
				
			||||||
 | 
					  content: string | MultiBlockContent[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AnthropicChatRequest {
 | 
				
			||||||
 | 
					  model: string; // The model that will complete your prompt.
 | 
				
			||||||
 | 
					  messages: AnthropicMessage[]; // The prompt that you want Claude to complete.
 | 
				
			||||||
 | 
					  max_tokens: number; // The maximum number of tokens to generate before stopping.
 | 
				
			||||||
 | 
					  stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
 | 
				
			||||||
 | 
					  temperature?: number; // Amount of randomness injected into the response.
 | 
				
			||||||
 | 
					  top_p?: number; // Use nucleus sampling.
 | 
				
			||||||
 | 
					  top_k?: number; // Only sample from the top K options for each subsequent token.
 | 
				
			||||||
 | 
					  metadata?: object; // An object describing metadata about the request.
 | 
				
			||||||
 | 
					  stream?: boolean; // Whether to incrementally stream the response using server-sent events.
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatRequest {
 | 
				
			||||||
 | 
					  model: string; // The model that will complete your prompt.
 | 
				
			||||||
 | 
					  prompt: string; // The prompt that you want Claude to complete.
 | 
				
			||||||
 | 
					  max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping.
 | 
				
			||||||
 | 
					  stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
 | 
				
			||||||
 | 
					  temperature?: number; // Amount of randomness injected into the response.
 | 
				
			||||||
 | 
					  top_p?: number; // Use nucleus sampling.
 | 
				
			||||||
 | 
					  top_k?: number; // Only sample from the top K options for each subsequent token.
 | 
				
			||||||
 | 
					  metadata?: object; // An object describing metadata about the request.
 | 
				
			||||||
 | 
					  stream?: boolean; // Whether to incrementally stream the response using server-sent events.
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatResponse {
 | 
				
			||||||
 | 
					  completion: string;
 | 
				
			||||||
 | 
					  stop_reason: "stop_sequence" | "max_tokens";
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ChatStreamResponse = ChatResponse & {
 | 
				
			||||||
 | 
					  stop?: string;
 | 
				
			||||||
 | 
					  log_id: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ClaudeMapper = {
 | 
				
			||||||
 | 
					  assistant: "assistant",
 | 
				
			||||||
 | 
					  user: "user",
 | 
				
			||||||
 | 
					  system: "user",
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const keys = ["claude-2, claude-instant-1"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ClaudeApi implements LLMApi {
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    console.log("[Response] claude response: ", res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return res?.content?.[0]?.text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions): Promise<void> {
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // try get base64image from local cache image_url
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = await preProcessImageContent(v.content);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const keys = ["system", "user"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
 | 
				
			||||||
 | 
					    for (let i = 0; i < messages.length - 1; i++) {
 | 
				
			||||||
 | 
					      const message = messages[i];
 | 
				
			||||||
 | 
					      const nextMessage = messages[i + 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
 | 
				
			||||||
 | 
					        messages[i] = [
 | 
				
			||||||
 | 
					          message,
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            role: "assistant",
 | 
				
			||||||
 | 
					            content: ";",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ] as any;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const prompt = messages
 | 
				
			||||||
 | 
					      .flat()
 | 
				
			||||||
 | 
					      .filter((v) => {
 | 
				
			||||||
 | 
					        if (!v.content) return false;
 | 
				
			||||||
 | 
					        if (typeof v.content === "string" && !v.content.trim()) return false;
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .map((v) => {
 | 
				
			||||||
 | 
					        const { role, content } = v;
 | 
				
			||||||
 | 
					        const insideRole = ClaudeMapper[role] ?? "user";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!visionModel || typeof content === "string") {
 | 
				
			||||||
 | 
					          return {
 | 
				
			||||||
 | 
					            role: insideRole,
 | 
				
			||||||
 | 
					            content: getMessageTextContent(v),
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          role: insideRole,
 | 
				
			||||||
 | 
					          content: content
 | 
				
			||||||
 | 
					            .filter((v) => v.image_url || v.text)
 | 
				
			||||||
 | 
					            .map(({ type, text, image_url }) => {
 | 
				
			||||||
 | 
					              if (type === "text") {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                  type,
 | 
				
			||||||
 | 
					                  text: text!,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              const { url = "" } = image_url || {};
 | 
				
			||||||
 | 
					              const colonIndex = url.indexOf(":");
 | 
				
			||||||
 | 
					              const semicolonIndex = url.indexOf(";");
 | 
				
			||||||
 | 
					              const comma = url.indexOf(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              const mimeType = url.slice(colonIndex + 1, semicolonIndex);
 | 
				
			||||||
 | 
					              const encodeType = url.slice(semicolonIndex + 1, comma);
 | 
				
			||||||
 | 
					              const data = url.slice(comma + 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                type: "image" as const,
 | 
				
			||||||
 | 
					                source: {
 | 
				
			||||||
 | 
					                  type: encodeType,
 | 
				
			||||||
 | 
					                  media_type: mimeType,
 | 
				
			||||||
 | 
					                  data,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (prompt[0]?.role === "assistant") {
 | 
				
			||||||
 | 
					      prompt.unshift({
 | 
				
			||||||
 | 
					        role: "user",
 | 
				
			||||||
 | 
					        content: ";",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestBody: AnthropicChatRequest = {
 | 
				
			||||||
 | 
					      messages: prompt,
 | 
				
			||||||
 | 
					      stream: shouldStream,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      max_tokens: modelConfig.max_tokens,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // top_k: modelConfig.top_k,
 | 
				
			||||||
 | 
					      top_k: 5,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const path = this.path(Anthropic.ChatPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (shouldStream) {
 | 
				
			||||||
 | 
					      let index = -1;
 | 
				
			||||||
 | 
					      const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					        .getState()
 | 
				
			||||||
 | 
					        .getAsTools(
 | 
				
			||||||
 | 
					          useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      return stream(
 | 
				
			||||||
 | 
					        path,
 | 
				
			||||||
 | 
					        requestBody,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          ...getHeaders(),
 | 
				
			||||||
 | 
					          "anthropic-version": accessStore.anthropicApiVersion,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
 | 
					        tools.map((tool) => ({
 | 
				
			||||||
 | 
					          name: tool?.function?.name,
 | 
				
			||||||
 | 
					          description: tool?.function?.description,
 | 
				
			||||||
 | 
					          input_schema: tool?.function?.parameters,
 | 
				
			||||||
 | 
					        })),
 | 
				
			||||||
 | 
					        funcs,
 | 
				
			||||||
 | 
					        controller,
 | 
				
			||||||
 | 
					        // parseSSE
 | 
				
			||||||
 | 
					        (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					          // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					          let chunkJson:
 | 
				
			||||||
 | 
					            | undefined
 | 
				
			||||||
 | 
					            | {
 | 
				
			||||||
 | 
					                type: "content_block_delta" | "content_block_stop";
 | 
				
			||||||
 | 
					                content_block?: {
 | 
				
			||||||
 | 
					                  type: "tool_use";
 | 
				
			||||||
 | 
					                  id: string;
 | 
				
			||||||
 | 
					                  name: string;
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                delta?: {
 | 
				
			||||||
 | 
					                  type: "text_delta" | "input_json_delta";
 | 
				
			||||||
 | 
					                  text?: string;
 | 
				
			||||||
 | 
					                  partial_json?: string;
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                index: number;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					          chunkJson = JSON.parse(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (chunkJson?.content_block?.type == "tool_use") {
 | 
				
			||||||
 | 
					            index += 1;
 | 
				
			||||||
 | 
					            const id = chunkJson?.content_block.id;
 | 
				
			||||||
 | 
					            const name = chunkJson?.content_block.name;
 | 
				
			||||||
 | 
					            runTools.push({
 | 
				
			||||||
 | 
					              id,
 | 
				
			||||||
 | 
					              type: "function",
 | 
				
			||||||
 | 
					              function: {
 | 
				
			||||||
 | 
					                name,
 | 
				
			||||||
 | 
					                arguments: "",
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (
 | 
				
			||||||
 | 
					            chunkJson?.delta?.type == "input_json_delta" &&
 | 
				
			||||||
 | 
					            chunkJson?.delta?.partial_json
 | 
				
			||||||
 | 
					          ) {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            runTools[index]["function"]["arguments"] +=
 | 
				
			||||||
 | 
					              chunkJson?.delta?.partial_json;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return chunkJson?.delta?.text;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					          requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					          toolCallMessage: any,
 | 
				
			||||||
 | 
					          toolCallResult: any[],
 | 
				
			||||||
 | 
					        ) => {
 | 
				
			||||||
 | 
					          // reset index value
 | 
				
			||||||
 | 
					          index = -1;
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
 | 
					          requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					            0,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              role: "assistant",
 | 
				
			||||||
 | 
					              content: toolCallMessage.tool_calls.map(
 | 
				
			||||||
 | 
					                (tool: ChatMessageTool) => ({
 | 
				
			||||||
 | 
					                  type: "tool_use",
 | 
				
			||||||
 | 
					                  id: tool.id,
 | 
				
			||||||
 | 
					                  name: tool?.function?.name,
 | 
				
			||||||
 | 
					                  input: tool?.function?.arguments
 | 
				
			||||||
 | 
					                    ? JSON.parse(tool?.function?.arguments)
 | 
				
			||||||
 | 
					                    : {},
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            ...toolCallResult.map((result) => ({
 | 
				
			||||||
 | 
					              role: "user",
 | 
				
			||||||
 | 
					              content: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                  type: "tool_result",
 | 
				
			||||||
 | 
					                  tool_use_id: result.tool_call_id,
 | 
				
			||||||
 | 
					                  content: result.content,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            })),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        options,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const payload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestBody),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          ...getHeaders(), // get common headers
 | 
				
			||||||
 | 
					          "anthropic-version": accessStore.anthropicApiVersion,
 | 
				
			||||||
 | 
					          // do not send `anthropicApiKey` in browser!!!
 | 
				
			||||||
 | 
					          // Authorization: getAuthKey(accessStore.anthropicApiKey),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        controller.signal.onabort = () =>
 | 
				
			||||||
 | 
					          options.onFinish("", new Response(null, { status: 400 }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const res = await fetch(path, payload);
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error("failed to chat", e);
 | 
				
			||||||
 | 
					        options.onError?.(e as Error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async models() {
 | 
				
			||||||
 | 
					    // const provider = {
 | 
				
			||||||
 | 
					    //   id: "anthropic",
 | 
				
			||||||
 | 
					    //   providerName: "Anthropic",
 | 
				
			||||||
 | 
					    //   providerType: "anthropic",
 | 
				
			||||||
 | 
					    // };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      // {
 | 
				
			||||||
 | 
					      //   name: "claude-instant-1.2",
 | 
				
			||||||
 | 
					      //   available: true,
 | 
				
			||||||
 | 
					      //   provider,
 | 
				
			||||||
 | 
					      // },
 | 
				
			||||||
 | 
					      // {
 | 
				
			||||||
 | 
					      //   name: "claude-2.0",
 | 
				
			||||||
 | 
					      //   available: true,
 | 
				
			||||||
 | 
					      //   provider,
 | 
				
			||||||
 | 
					      // },
 | 
				
			||||||
 | 
					      // {
 | 
				
			||||||
 | 
					      //   name: "claude-2.1",
 | 
				
			||||||
 | 
					      //   available: true,
 | 
				
			||||||
 | 
					      //   provider,
 | 
				
			||||||
 | 
					      // },
 | 
				
			||||||
 | 
					      // {
 | 
				
			||||||
 | 
					      //   name: "claude-3-opus-20240229",
 | 
				
			||||||
 | 
					      //   available: true,
 | 
				
			||||||
 | 
					      //   provider,
 | 
				
			||||||
 | 
					      // },
 | 
				
			||||||
 | 
					      // {
 | 
				
			||||||
 | 
					      //   name: "claude-3-sonnet-20240229",
 | 
				
			||||||
 | 
					      //   available: true,
 | 
				
			||||||
 | 
					      //   provider,
 | 
				
			||||||
 | 
					      // },
 | 
				
			||||||
 | 
					      // {
 | 
				
			||||||
 | 
					      //   name: "claude-3-haiku-20240307",
 | 
				
			||||||
 | 
					      //   available: true,
 | 
				
			||||||
 | 
					      //   provider,
 | 
				
			||||||
 | 
					      // },
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl: string = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.anthropicUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // if endpoint is empty, use default endpoint
 | 
				
			||||||
 | 
					    if (baseUrl.trim().length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    baseUrl = trimEnd(baseUrl, "/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // try rebuild url, when using cloudflare ai gateway in client
 | 
				
			||||||
 | 
					    return cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function trimEnd(s: string, end = " ") {
 | 
				
			||||||
 | 
					  if (end.length === 0) return s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  while (s.endsWith(end)) {
 | 
				
			||||||
 | 
					    s = s.slice(0, -end.length);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										284
									
								
								app/client/platforms/baidu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								app/client/platforms/baidu.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,284 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, Baidu, BAIDU_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
 | 
					import { getAccessToken } from "@/app/utils/baidu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import Locale from "../../locales";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  EventStreamContentType,
 | 
				
			||||||
 | 
					  fetchEventSource,
 | 
				
			||||||
 | 
					} from "@fortaine/fetch-event-source";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { getMessageTextContent, getTimeoutMSByModel } from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RequestPayload {
 | 
				
			||||||
 | 
					  messages: {
 | 
				
			||||||
 | 
					    role: "system" | "user" | "assistant";
 | 
				
			||||||
 | 
					    content: string | MultimodalContent[];
 | 
				
			||||||
 | 
					  }[];
 | 
				
			||||||
 | 
					  stream?: boolean;
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  temperature: number;
 | 
				
			||||||
 | 
					  presence_penalty: number;
 | 
				
			||||||
 | 
					  frequency_penalty: number;
 | 
				
			||||||
 | 
					  top_p: number;
 | 
				
			||||||
 | 
					  max_tokens?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ErnieApi implements LLMApi {
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.baiduUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      // do not use proxy for baidubce api
 | 
				
			||||||
 | 
					      baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Baidu)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const messages = options.messages.map((v) => ({
 | 
				
			||||||
 | 
					      // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
 | 
				
			||||||
 | 
					      role: v.role === "system" ? "user" : v.role,
 | 
				
			||||||
 | 
					      content: getMessageTextContent(v),
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // "error_code": 336006, "error_msg": "the length of messages must be an odd number",
 | 
				
			||||||
 | 
					    if (messages.length % 2 === 0) {
 | 
				
			||||||
 | 
					      if (messages.at(0)?.role === "user") {
 | 
				
			||||||
 | 
					        messages.splice(1, 0, {
 | 
				
			||||||
 | 
					          role: "assistant",
 | 
				
			||||||
 | 
					          content: " ",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        messages.unshift({
 | 
				
			||||||
 | 
					          role: "user",
 | 
				
			||||||
 | 
					          content: " ",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: shouldStream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] Baidu payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      let chatPath = this.path(Baidu.ChatPath(modelConfig.model));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // getAccessToken can not run in browser, because cors error
 | 
				
			||||||
 | 
					      if (!!getClientConfig()?.isApp) {
 | 
				
			||||||
 | 
					        const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					        if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					          if (accessStore.isValidBaidu()) {
 | 
				
			||||||
 | 
					            const { access_token } = await getAccessToken(
 | 
				
			||||||
 | 
					              accessStore.baiduApiKey,
 | 
				
			||||||
 | 
					              accessStore.baiduSecretKey,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            chatPath = `${chatPath}${
 | 
				
			||||||
 | 
					              chatPath.includes("?") ? "&" : "?"
 | 
				
			||||||
 | 
					            }access_token=${access_token}`;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        let responseText = "";
 | 
				
			||||||
 | 
					        let remainText = "";
 | 
				
			||||||
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        let responseRes: Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // animate response to make it looks smooth
 | 
				
			||||||
 | 
					        function animateResponseText() {
 | 
				
			||||||
 | 
					          if (finished || controller.signal.aborted) {
 | 
				
			||||||
 | 
					            responseText += remainText;
 | 
				
			||||||
 | 
					            console.log("[Response Animation] finished");
 | 
				
			||||||
 | 
					            if (responseText?.length === 0) {
 | 
				
			||||||
 | 
					              options.onError?.(new Error("empty response from server"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (remainText.length > 0) {
 | 
				
			||||||
 | 
					            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
				
			||||||
 | 
					            const fetchText = remainText.slice(0, fetchCount);
 | 
				
			||||||
 | 
					            responseText += fetchText;
 | 
				
			||||||
 | 
					            remainText = remainText.slice(fetchCount);
 | 
				
			||||||
 | 
					            options.onUpdate?.(responseText, fetchText);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          requestAnimationFrame(animateResponseText);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // start animaion
 | 
				
			||||||
 | 
					        animateResponseText();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const finish = () => {
 | 
				
			||||||
 | 
					          if (!finished) {
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					            options.onFinish(responseText + remainText, responseRes);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          fetch: fetch as any,
 | 
				
			||||||
 | 
					          ...chatPayload,
 | 
				
			||||||
 | 
					          async onopen(res) {
 | 
				
			||||||
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					            const contentType = res.headers.get("content-type");
 | 
				
			||||||
 | 
					            console.log("[Baidu] request response content type: ", contentType);
 | 
				
			||||||
 | 
					            responseRes = res;
 | 
				
			||||||
 | 
					            if (contentType?.startsWith("text/plain")) {
 | 
				
			||||||
 | 
					              responseText = await res.clone().text();
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              !res.ok ||
 | 
				
			||||||
 | 
					              !res.headers
 | 
				
			||||||
 | 
					                .get("content-type")
 | 
				
			||||||
 | 
					                ?.startsWith(EventStreamContentType) ||
 | 
				
			||||||
 | 
					              res.status !== 200
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              const responseTexts = [responseText];
 | 
				
			||||||
 | 
					              let extraInfo = await res.clone().text();
 | 
				
			||||||
 | 
					              try {
 | 
				
			||||||
 | 
					                const resJson = await res.clone().json();
 | 
				
			||||||
 | 
					                extraInfo = prettyObject(resJson);
 | 
				
			||||||
 | 
					              } catch {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (res.status === 401) {
 | 
				
			||||||
 | 
					                responseTexts.push(Locale.Error.Unauthorized);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (extraInfo) {
 | 
				
			||||||
 | 
					                responseTexts.push(extraInfo);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              responseText = responseTexts.join("\n\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onmessage(msg) {
 | 
				
			||||||
 | 
					            if (msg.data === "[DONE]" || finished) {
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const text = msg.data;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              const json = JSON.parse(text);
 | 
				
			||||||
 | 
					              const delta = json?.result;
 | 
				
			||||||
 | 
					              if (delta) {
 | 
				
			||||||
 | 
					                remainText += delta;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					              console.error("[Request] parse error", text, msg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onclose() {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onerror(e) {
 | 
				
			||||||
 | 
					            options.onError?.(e);
 | 
				
			||||||
 | 
					            throw e;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          openWhenHidden: true,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = resJson?.result;
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export { Baidu };
 | 
				
			||||||
							
								
								
									
										250
									
								
								app/client/platforms/bytedance.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								app/client/platforms/bytedance.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,250 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, ByteDance, BYTEDANCE_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { streamWithThink } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { preProcessImageContent } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RequestPayloadForByteDance {
 | 
				
			||||||
 | 
					  messages: {
 | 
				
			||||||
 | 
					    role: "system" | "user" | "assistant";
 | 
				
			||||||
 | 
					    content: string | MultimodalContent[];
 | 
				
			||||||
 | 
					  }[];
 | 
				
			||||||
 | 
					  stream?: boolean;
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  temperature: number;
 | 
				
			||||||
 | 
					  presence_penalty: number;
 | 
				
			||||||
 | 
					  frequency_penalty: number;
 | 
				
			||||||
 | 
					  top_p: number;
 | 
				
			||||||
 | 
					  max_tokens?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DoubaoApi implements LLMApi {
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.bytedanceUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content =
 | 
				
			||||||
 | 
					        v.role === "assistant"
 | 
				
			||||||
 | 
					          ? getMessageTextContentWithoutThinking(v)
 | 
				
			||||||
 | 
					          : await preProcessImageContent(v.content);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayloadForByteDance = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: shouldStream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(ByteDance.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string | null;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayloadForByteDance,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export { ByteDance };
 | 
				
			||||||
							
								
								
									
										253
									
								
								app/client/platforms/deepseek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								app/client/platforms/deepseek.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,253 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import { ApiPath, DEEPSEEK_BASE_URL, DeepSeek } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { streamWithThink } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DeepSeekApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.deepseekUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.DeepSeek;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? DEEPSEEK_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.DeepSeek)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      if (v.role === "assistant") {
 | 
				
			||||||
 | 
					        const content = getMessageTextContentWithoutThinking(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const content = getMessageTextContent(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 检测并修复消息顺序,确保除system外的第一个消息是user
 | 
				
			||||||
 | 
					    const filteredMessages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    let hasFoundFirstUser = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const msg of messages) {
 | 
				
			||||||
 | 
					      if (msg.role === "system") {
 | 
				
			||||||
 | 
					        // Keep all system messages
 | 
				
			||||||
 | 
					        filteredMessages.push(msg);
 | 
				
			||||||
 | 
					      } else if (msg.role === "user") {
 | 
				
			||||||
 | 
					        // User message directly added
 | 
				
			||||||
 | 
					        filteredMessages.push(msg);
 | 
				
			||||||
 | 
					        hasFoundFirstUser = true;
 | 
				
			||||||
 | 
					      } else if (hasFoundFirstUser) {
 | 
				
			||||||
 | 
					        // After finding the first user message, all subsequent non-system messages are retained.
 | 
				
			||||||
 | 
					        filteredMessages.push(msg);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // If hasFoundFirstUser is false and it is not a system message, it will be skipped.
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages: filteredMessages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(DeepSeek.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string | null;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										292
									
								
								app/client/platforms/glm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								app/client/platforms/glm.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,292 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, CHATGLM_BASE_URL, ChatGLM } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { stream } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					import { preProcessImageContent } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BasePayload {
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChatPayload extends BasePayload {
 | 
				
			||||||
 | 
					  messages: ChatOptions["messages"];
 | 
				
			||||||
 | 
					  stream?: boolean;
 | 
				
			||||||
 | 
					  temperature?: number;
 | 
				
			||||||
 | 
					  presence_penalty?: number;
 | 
				
			||||||
 | 
					  frequency_penalty?: number;
 | 
				
			||||||
 | 
					  top_p?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ImageGenerationPayload extends BasePayload {
 | 
				
			||||||
 | 
					  prompt: string;
 | 
				
			||||||
 | 
					  size?: string;
 | 
				
			||||||
 | 
					  user_id?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface VideoGenerationPayload extends BasePayload {
 | 
				
			||||||
 | 
					  prompt: string;
 | 
				
			||||||
 | 
					  duration?: number;
 | 
				
			||||||
 | 
					  resolution?: string;
 | 
				
			||||||
 | 
					  user_id?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ModelType = "chat" | "image" | "video";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ChatGLMApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getModelType(model: string): ModelType {
 | 
				
			||||||
 | 
					    if (model.startsWith("cogview-")) return "image";
 | 
				
			||||||
 | 
					    if (model.startsWith("cogvideo-")) return "video";
 | 
				
			||||||
 | 
					    return "chat";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getModelPath(type: ModelType): string {
 | 
				
			||||||
 | 
					    switch (type) {
 | 
				
			||||||
 | 
					      case "image":
 | 
				
			||||||
 | 
					        return ChatGLM.ImagePath;
 | 
				
			||||||
 | 
					      case "video":
 | 
				
			||||||
 | 
					        return ChatGLM.VideoPath;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return ChatGLM.ChatPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private createPayload(
 | 
				
			||||||
 | 
					    messages: ChatOptions["messages"],
 | 
				
			||||||
 | 
					    modelConfig: any,
 | 
				
			||||||
 | 
					    options: ChatOptions,
 | 
				
			||||||
 | 
					  ): BasePayload {
 | 
				
			||||||
 | 
					    const modelType = this.getModelType(modelConfig.model);
 | 
				
			||||||
 | 
					    const lastMessage = messages[messages.length - 1];
 | 
				
			||||||
 | 
					    const prompt =
 | 
				
			||||||
 | 
					      typeof lastMessage.content === "string"
 | 
				
			||||||
 | 
					        ? lastMessage.content
 | 
				
			||||||
 | 
					        : lastMessage.content.map((c) => c.text).join("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (modelType) {
 | 
				
			||||||
 | 
					      case "image":
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          model: modelConfig.model,
 | 
				
			||||||
 | 
					          prompt,
 | 
				
			||||||
 | 
					          size: options.config.size,
 | 
				
			||||||
 | 
					        } as ImageGenerationPayload;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          messages,
 | 
				
			||||||
 | 
					          stream: options.config.stream,
 | 
				
			||||||
 | 
					          model: modelConfig.model,
 | 
				
			||||||
 | 
					          temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					          presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					          frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					          top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					        } as ChatPayload;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private parseResponse(modelType: ModelType, json: any): string {
 | 
				
			||||||
 | 
					    switch (modelType) {
 | 
				
			||||||
 | 
					      case "image": {
 | 
				
			||||||
 | 
					        const imageUrl = json.data?.[0]?.url;
 | 
				
			||||||
 | 
					        return imageUrl ? `` : "";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "video": {
 | 
				
			||||||
 | 
					        const videoUrl = json.data?.[0]?.url;
 | 
				
			||||||
 | 
					        return videoUrl ? `<video controls src="${videoUrl}"></video>` : "";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return this.extractMessage(json);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.chatglmUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.ChatGLM;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = visionModel
 | 
				
			||||||
 | 
					        ? await preProcessImageContent(v.content)
 | 
				
			||||||
 | 
					        : getMessageTextContent(v);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const modelType = this.getModelType(modelConfig.model);
 | 
				
			||||||
 | 
					    const requestPayload = this.createPayload(messages, modelConfig, options);
 | 
				
			||||||
 | 
					    const path = this.path(this.getModelPath(modelType));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`[Request] glm ${modelType} payload: `, requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (modelType === "image" || modelType === "video") {
 | 
				
			||||||
 | 
					        const res = await fetch(path, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        console.log(`[Response] glm ${modelType}:`, resJson);
 | 
				
			||||||
 | 
					        const message = this.parseResponse(modelType, resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return stream(
 | 
				
			||||||
 | 
					          path,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(path, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										317
									
								
								app/client/platforms/google.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								app/client/platforms/google.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,317 @@
 | 
				
			|||||||
 | 
					import { ApiPath, Google } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  LLMUsage,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { stream } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { GEMINI_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  getMessageImages,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { preProcessImageContent } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { nanoid } from "nanoid";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class GeminiProApi implements LLMApi {
 | 
				
			||||||
 | 
					  path(path: string, shouldStream = false): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.googleUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Google)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let chatPath = [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					    if (shouldStream) {
 | 
				
			||||||
 | 
					      chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return chatPath;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    console.log("[Response] gemini-pro response: ", res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getTextFromParts = (parts: any[]) => {
 | 
				
			||||||
 | 
					      if (!Array.isArray(parts)) return "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return parts
 | 
				
			||||||
 | 
					        .map((part) => part?.text || "")
 | 
				
			||||||
 | 
					        .filter((text) => text.trim() !== "")
 | 
				
			||||||
 | 
					        .join("\n\n");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let content = "";
 | 
				
			||||||
 | 
					    if (Array.isArray(res)) {
 | 
				
			||||||
 | 
					      res.map((item) => {
 | 
				
			||||||
 | 
					        content += getTextFromParts(item?.candidates?.at(0)?.content?.parts);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      getTextFromParts(res?.candidates?.at(0)?.content?.parts) ||
 | 
				
			||||||
 | 
					      content || //getTextFromParts(res?.at(0)?.candidates?.at(0)?.content?.parts) ||
 | 
				
			||||||
 | 
					      res?.error?.message ||
 | 
				
			||||||
 | 
					      ""
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions): Promise<void> {
 | 
				
			||||||
 | 
					    const apiClient = this;
 | 
				
			||||||
 | 
					    let multimodal = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // try get base64image from local cache image_url
 | 
				
			||||||
 | 
					    const _messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = await preProcessImageContent(v.content);
 | 
				
			||||||
 | 
					      _messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const messages = _messages.map((v) => {
 | 
				
			||||||
 | 
					      let parts: any[] = [{ text: getMessageTextContent(v) }];
 | 
				
			||||||
 | 
					      if (isVisionModel(options.config.model)) {
 | 
				
			||||||
 | 
					        const images = getMessageImages(v);
 | 
				
			||||||
 | 
					        if (images.length > 0) {
 | 
				
			||||||
 | 
					          multimodal = true;
 | 
				
			||||||
 | 
					          parts = parts.concat(
 | 
				
			||||||
 | 
					            images.map((image) => {
 | 
				
			||||||
 | 
					              const imageType = image.split(";")[0].split(":")[1];
 | 
				
			||||||
 | 
					              const imageData = image.split(",")[1];
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                inline_data: {
 | 
				
			||||||
 | 
					                  mime_type: imageType,
 | 
				
			||||||
 | 
					                  data: imageData,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        role: v.role.replace("assistant", "model").replace("system", "user"),
 | 
				
			||||||
 | 
					        parts: parts,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // google requires that role in neighboring messages must not be the same
 | 
				
			||||||
 | 
					    for (let i = 0; i < messages.length - 1; ) {
 | 
				
			||||||
 | 
					      // Check if current and next item both have the role "model"
 | 
				
			||||||
 | 
					      if (messages[i].role === messages[i + 1].role) {
 | 
				
			||||||
 | 
					        // Concatenate the 'parts' of the current and next item
 | 
				
			||||||
 | 
					        messages[i].parts = messages[i].parts.concat(messages[i + 1].parts);
 | 
				
			||||||
 | 
					        // Remove the next item
 | 
				
			||||||
 | 
					        messages.splice(i + 1, 1);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Move to the next item
 | 
				
			||||||
 | 
					        i++;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // if (visionModel && messages.length > 1) {
 | 
				
			||||||
 | 
					    //   options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const requestPayload = {
 | 
				
			||||||
 | 
					      contents: messages,
 | 
				
			||||||
 | 
					      generationConfig: {
 | 
				
			||||||
 | 
					        // stopSequences: [
 | 
				
			||||||
 | 
					        //   "Title"
 | 
				
			||||||
 | 
					        // ],
 | 
				
			||||||
 | 
					        temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					        maxOutputTokens: modelConfig.max_tokens,
 | 
				
			||||||
 | 
					        topP: modelConfig.top_p,
 | 
				
			||||||
 | 
					        // "topK": modelConfig.top_k,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      safetySettings: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          category: "HARM_CATEGORY_HARASSMENT",
 | 
				
			||||||
 | 
					          threshold: accessStore.googleSafetySettings,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          category: "HARM_CATEGORY_HATE_SPEECH",
 | 
				
			||||||
 | 
					          threshold: accessStore.googleSafetySettings,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
 | 
				
			||||||
 | 
					          threshold: accessStore.googleSafetySettings,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          category: "HARM_CATEGORY_DANGEROUS_CONTENT",
 | 
				
			||||||
 | 
					          threshold: accessStore.googleSafetySettings,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
 | 
				
			||||||
 | 
					      const chatPath = this.path(
 | 
				
			||||||
 | 
					        Google.ChatPath(modelConfig.model),
 | 
				
			||||||
 | 
					        shouldStream,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const isThinking = options.config.model.includes("-thinking");
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return stream(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
 | 
					          tools.length > 0
 | 
				
			||||||
 | 
					            ? // @ts-ignore
 | 
				
			||||||
 | 
					              [{ functionDeclarations: tools.map((tool) => tool.function) }]
 | 
				
			||||||
 | 
					            : [],
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const chunkJson = JSON.parse(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const functionCall = chunkJson?.candidates
 | 
				
			||||||
 | 
					              ?.at(0)
 | 
				
			||||||
 | 
					              ?.content.parts.at(0)?.functionCall;
 | 
				
			||||||
 | 
					            if (functionCall) {
 | 
				
			||||||
 | 
					              const { name, args } = functionCall;
 | 
				
			||||||
 | 
					              runTools.push({
 | 
				
			||||||
 | 
					                id: nanoid(),
 | 
				
			||||||
 | 
					                type: "function",
 | 
				
			||||||
 | 
					                function: {
 | 
				
			||||||
 | 
					                  name,
 | 
				
			||||||
 | 
					                  arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return chunkJson?.candidates
 | 
				
			||||||
 | 
					              ?.at(0)
 | 
				
			||||||
 | 
					              ?.content.parts?.map((part: { text: string }) => part.text)
 | 
				
			||||||
 | 
					              .join("\n\n");
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.contents?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.contents?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                role: "model",
 | 
				
			||||||
 | 
					                parts: toolCallMessage.tool_calls.map(
 | 
				
			||||||
 | 
					                  (tool: ChatMessageTool) => ({
 | 
				
			||||||
 | 
					                    functionCall: {
 | 
				
			||||||
 | 
					                      name: tool?.function?.name,
 | 
				
			||||||
 | 
					                      args: JSON.parse(tool?.function?.arguments as string),
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  }),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              ...toolCallResult.map((result) => ({
 | 
				
			||||||
 | 
					                role: "function",
 | 
				
			||||||
 | 
					                parts: [
 | 
				
			||||||
 | 
					                  {
 | 
				
			||||||
 | 
					                    functionResponse: {
 | 
				
			||||||
 | 
					                      name: result.name,
 | 
				
			||||||
 | 
					                      response: {
 | 
				
			||||||
 | 
					                        name: result.name,
 | 
				
			||||||
 | 
					                        content: result.content, // TODO just text content...
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              })),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        if (resJson?.promptFeedback?.blockReason) {
 | 
				
			||||||
 | 
					          // being blocked
 | 
				
			||||||
 | 
					          options.onError?.(
 | 
				
			||||||
 | 
					            new Error(
 | 
				
			||||||
 | 
					              "Message is being blocked for reason: " +
 | 
				
			||||||
 | 
					                resJson.promptFeedback.blockReason,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const message = apiClient.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  usage(): Promise<LLMUsage> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										253
									
								
								app/client/platforms/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								app/client/platforms/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,253 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  IFLYTEK_BASE_URL,
 | 
				
			||||||
 | 
					  Iflytek,
 | 
				
			||||||
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import Locale from "../../locales";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  EventStreamContentType,
 | 
				
			||||||
 | 
					  fetchEventSource,
 | 
				
			||||||
 | 
					} from "@fortaine/fetch-event-source";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { getMessageTextContent } from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SparkApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.iflytekUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.Iflytek;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = getMessageTextContent(v);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] Spark payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(Iflytek.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        let responseText = "";
 | 
				
			||||||
 | 
					        let remainText = "";
 | 
				
			||||||
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        let responseRes: Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Animate response text to make it look smooth
 | 
				
			||||||
 | 
					        function animateResponseText() {
 | 
				
			||||||
 | 
					          if (finished || controller.signal.aborted) {
 | 
				
			||||||
 | 
					            responseText += remainText;
 | 
				
			||||||
 | 
					            console.log("[Response Animation] finished");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (remainText.length > 0) {
 | 
				
			||||||
 | 
					            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
				
			||||||
 | 
					            const fetchText = remainText.slice(0, fetchCount);
 | 
				
			||||||
 | 
					            responseText += fetchText;
 | 
				
			||||||
 | 
					            remainText = remainText.slice(fetchCount);
 | 
				
			||||||
 | 
					            options.onUpdate?.(responseText, fetchText);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          requestAnimationFrame(animateResponseText);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Start animation
 | 
				
			||||||
 | 
					        animateResponseText();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const finish = () => {
 | 
				
			||||||
 | 
					          if (!finished) {
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					            options.onFinish(responseText + remainText, responseRes);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          fetch: fetch as any,
 | 
				
			||||||
 | 
					          ...chatPayload,
 | 
				
			||||||
 | 
					          async onopen(res) {
 | 
				
			||||||
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					            const contentType = res.headers.get("content-type");
 | 
				
			||||||
 | 
					            console.log("[Spark] request response content type: ", contentType);
 | 
				
			||||||
 | 
					            responseRes = res;
 | 
				
			||||||
 | 
					            if (contentType?.startsWith("text/plain")) {
 | 
				
			||||||
 | 
					              responseText = await res.clone().text();
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Handle different error scenarios
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              !res.ok ||
 | 
				
			||||||
 | 
					              !res.headers
 | 
				
			||||||
 | 
					                .get("content-type")
 | 
				
			||||||
 | 
					                ?.startsWith(EventStreamContentType) ||
 | 
				
			||||||
 | 
					              res.status !== 200
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              let extraInfo = await res.clone().text();
 | 
				
			||||||
 | 
					              try {
 | 
				
			||||||
 | 
					                const resJson = await res.clone().json();
 | 
				
			||||||
 | 
					                extraInfo = prettyObject(resJson);
 | 
				
			||||||
 | 
					              } catch {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (res.status === 401) {
 | 
				
			||||||
 | 
					                extraInfo = Locale.Error.Unauthorized;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              options.onError?.(
 | 
				
			||||||
 | 
					                new Error(
 | 
				
			||||||
 | 
					                  `Request failed with status ${res.status}: ${extraInfo}`,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onmessage(msg) {
 | 
				
			||||||
 | 
					            if (msg.data === "[DONE]" || finished) {
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const text = msg.data;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              const json = JSON.parse(text);
 | 
				
			||||||
 | 
					              const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					                delta: { content: string };
 | 
				
			||||||
 | 
					              }>;
 | 
				
			||||||
 | 
					              const delta = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (delta) {
 | 
				
			||||||
 | 
					                remainText += delta;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					              console.error("[Request] parse error", text);
 | 
				
			||||||
 | 
					              options.onError?.(new Error(`Failed to parse response: ${text}`));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onclose() {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onerror(e) {
 | 
				
			||||||
 | 
					            options.onError?.(e);
 | 
				
			||||||
 | 
					            throw e;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          openWhenHidden: true,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!res.ok) {
 | 
				
			||||||
 | 
					          const errorText = await res.text();
 | 
				
			||||||
 | 
					          options.onError?.(
 | 
				
			||||||
 | 
					            new Error(`Request failed with status ${res.status}: ${errorText}`),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										200
									
								
								app/client/platforms/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								app/client/platforms/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,200 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  MOONSHOT_BASE_URL,
 | 
				
			||||||
 | 
					  Moonshot,
 | 
				
			||||||
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { stream } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { getMessageTextContent } from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class MoonshotApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.moonshotUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.Moonshot;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = getMessageTextContent(v);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(Moonshot.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return stream(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										518
									
								
								app/client/platforms/openai.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										518
									
								
								app/client/platforms/openai.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,518 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  OPENAI_BASE_URL,
 | 
				
			||||||
 | 
					  DEFAULT_MODELS,
 | 
				
			||||||
 | 
					  OpenaiPath,
 | 
				
			||||||
 | 
					  Azure,
 | 
				
			||||||
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { collectModelsWithDefaultModel } from "@/app/utils/model";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  preProcessImageContent,
 | 
				
			||||||
 | 
					  uploadImage,
 | 
				
			||||||
 | 
					  base64Image2Blob,
 | 
				
			||||||
 | 
					  streamWithThink,
 | 
				
			||||||
 | 
					} from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
				
			||||||
 | 
					import { ModelSize, DalleQuality, DalleStyle } from "@/app/typing";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  LLMUsage,
 | 
				
			||||||
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import Locale from "../../locales";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  isDalle3 as _isDalle3,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RequestPayload {
 | 
				
			||||||
 | 
					  messages: {
 | 
				
			||||||
 | 
					    role: "system" | "user" | "assistant";
 | 
				
			||||||
 | 
					    content: string | MultimodalContent[];
 | 
				
			||||||
 | 
					  }[];
 | 
				
			||||||
 | 
					  stream?: boolean;
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  temperature: number;
 | 
				
			||||||
 | 
					  presence_penalty: number;
 | 
				
			||||||
 | 
					  frequency_penalty: number;
 | 
				
			||||||
 | 
					  top_p: number;
 | 
				
			||||||
 | 
					  max_tokens?: number;
 | 
				
			||||||
 | 
					  max_completion_tokens?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface DalleRequestPayload {
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  prompt: string;
 | 
				
			||||||
 | 
					  response_format: "url" | "b64_json";
 | 
				
			||||||
 | 
					  n: number;
 | 
				
			||||||
 | 
					  size: ModelSize;
 | 
				
			||||||
 | 
					  quality: DalleQuality;
 | 
				
			||||||
 | 
					  style: DalleStyle;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ChatGPTApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isAzure = path.includes("deployments");
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      if (isAzure && !accessStore.isValidAzure()) {
 | 
				
			||||||
 | 
					        throw Error(
 | 
				
			||||||
 | 
					          "incomplete azure config, please check it in your settings page",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      !baseUrl.startsWith("http") &&
 | 
				
			||||||
 | 
					      !isAzure &&
 | 
				
			||||||
 | 
					      !baseUrl.startsWith(ApiPath.OpenAI)
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // try rebuild url, when using cloudflare ai gateway in client
 | 
				
			||||||
 | 
					    return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async extractMessage(res: any) {
 | 
				
			||||||
 | 
					    if (res.error) {
 | 
				
			||||||
 | 
					      return "```\n" + JSON.stringify(res, null, 4) + "\n```";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // dalle3 model return url, using url create image message
 | 
				
			||||||
 | 
					    if (res.data) {
 | 
				
			||||||
 | 
					      let url = res.data?.at(0)?.url ?? "";
 | 
				
			||||||
 | 
					      const b64_json = res.data?.at(0)?.b64_json ?? "";
 | 
				
			||||||
 | 
					      if (!url && b64_json) {
 | 
				
			||||||
 | 
					        // uploadImage
 | 
				
			||||||
 | 
					        url = await uploadImage(base64Image2Blob(b64_json, "image/png"));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: "image_url",
 | 
				
			||||||
 | 
					          image_url: {
 | 
				
			||||||
 | 
					            url,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? res;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    const requestPayload = {
 | 
				
			||||||
 | 
					      model: options.model,
 | 
				
			||||||
 | 
					      input: options.input,
 | 
				
			||||||
 | 
					      voice: options.voice,
 | 
				
			||||||
 | 
					      response_format: options.response_format,
 | 
				
			||||||
 | 
					      speed: options.speed,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai speech payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const speechPath = this.path(OpenaiPath.SpeechPath);
 | 
				
			||||||
 | 
					      const speechPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const res = await fetch(speechPath, speechPayload);
 | 
				
			||||||
 | 
					      clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					      return await res.arrayBuffer();
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a speech request", e);
 | 
				
			||||||
 | 
					      throw e;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let requestPayload: RequestPayload | DalleRequestPayload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isDalle3 = _isDalle3(options.config.model);
 | 
				
			||||||
 | 
					    const isO1OrO3 =
 | 
				
			||||||
 | 
					      options.config.model.startsWith("o1") ||
 | 
				
			||||||
 | 
					      options.config.model.startsWith("o3") ||
 | 
				
			||||||
 | 
					      options.config.model.startsWith("o4-mini");
 | 
				
			||||||
 | 
					    if (isDalle3) {
 | 
				
			||||||
 | 
					      const prompt = getMessageTextContent(
 | 
				
			||||||
 | 
					        options.messages.slice(-1)?.pop() as any,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      requestPayload = {
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        prompt,
 | 
				
			||||||
 | 
					        // URLs are only valid for 60 minutes after the image has been generated.
 | 
				
			||||||
 | 
					        response_format: "b64_json", // using b64_json, and save image in CacheStorage
 | 
				
			||||||
 | 
					        n: 1,
 | 
				
			||||||
 | 
					        size: options.config?.size ?? "1024x1024",
 | 
				
			||||||
 | 
					        quality: options.config?.quality ?? "standard",
 | 
				
			||||||
 | 
					        style: options.config?.style ?? "vivid",
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					      const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					      for (const v of options.messages) {
 | 
				
			||||||
 | 
					        const content = visionModel
 | 
				
			||||||
 | 
					          ? await preProcessImageContent(v.content)
 | 
				
			||||||
 | 
					          : getMessageTextContent(v);
 | 
				
			||||||
 | 
					        if (!(isO1OrO3 && v.role === "system"))
 | 
				
			||||||
 | 
					          messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
 | 
				
			||||||
 | 
					      requestPayload = {
 | 
				
			||||||
 | 
					        messages,
 | 
				
			||||||
 | 
					        stream: options.config.stream,
 | 
				
			||||||
 | 
					        model: modelConfig.model,
 | 
				
			||||||
 | 
					        temperature: !isO1OrO3 ? modelConfig.temperature : 1,
 | 
				
			||||||
 | 
					        presence_penalty: !isO1OrO3 ? modelConfig.presence_penalty : 0,
 | 
				
			||||||
 | 
					        frequency_penalty: !isO1OrO3 ? modelConfig.frequency_penalty : 0,
 | 
				
			||||||
 | 
					        top_p: !isO1OrO3 ? modelConfig.top_p : 1,
 | 
				
			||||||
 | 
					        // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					        // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs)
 | 
				
			||||||
 | 
					      if (isO1OrO3) {
 | 
				
			||||||
 | 
					        requestPayload["max_completion_tokens"] = modelConfig.max_tokens;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // add max_tokens to vision model
 | 
				
			||||||
 | 
					      if (visionModel && !isO1OrO3) {
 | 
				
			||||||
 | 
					        requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !isDalle3 && !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      let chatPath = "";
 | 
				
			||||||
 | 
					      if (modelConfig.providerName === ServiceProvider.Azure) {
 | 
				
			||||||
 | 
					        // find model, and get displayName as deployName
 | 
				
			||||||
 | 
					        const { models: configModels, customModels: configCustomModels } =
 | 
				
			||||||
 | 
					          useAppConfig.getState();
 | 
				
			||||||
 | 
					        const {
 | 
				
			||||||
 | 
					          defaultModel,
 | 
				
			||||||
 | 
					          customModels: accessCustomModels,
 | 
				
			||||||
 | 
					          useCustomConfig,
 | 
				
			||||||
 | 
					        } = useAccessStore.getState();
 | 
				
			||||||
 | 
					        const models = collectModelsWithDefaultModel(
 | 
				
			||||||
 | 
					          configModels,
 | 
				
			||||||
 | 
					          [configCustomModels, accessCustomModels].join(","),
 | 
				
			||||||
 | 
					          defaultModel,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        const model = models.find(
 | 
				
			||||||
 | 
					          (model) =>
 | 
				
			||||||
 | 
					            model.name === modelConfig.model &&
 | 
				
			||||||
 | 
					            model?.provider?.providerName === ServiceProvider.Azure,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        chatPath = this.path(
 | 
				
			||||||
 | 
					          (isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
 | 
				
			||||||
 | 
					            (model?.displayName ?? model?.name) as string,
 | 
				
			||||||
 | 
					            useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        chatPath = this.path(
 | 
				
			||||||
 | 
					          isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        let index = -1;
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        // console.log("getAsTools", tools, funcs);
 | 
				
			||||||
 | 
					        streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                index += 1;
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // reset index value
 | 
				
			||||||
 | 
					            index = -1;
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const chatPayload = {
 | 
				
			||||||
 | 
					          method: "POST",
 | 
				
			||||||
 | 
					          body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					          signal: controller.signal,
 | 
				
			||||||
 | 
					          headers: getHeaders(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // make a fetch request
 | 
				
			||||||
 | 
					        const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					          () => controller.abort(),
 | 
				
			||||||
 | 
					          getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = await this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    const formatDate = (d: Date) =>
 | 
				
			||||||
 | 
					      `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
 | 
				
			||||||
 | 
					        .getDate()
 | 
				
			||||||
 | 
					        .toString()
 | 
				
			||||||
 | 
					        .padStart(2, "0")}`;
 | 
				
			||||||
 | 
					    const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					    const now = new Date();
 | 
				
			||||||
 | 
					    const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
 | 
				
			||||||
 | 
					    const startDate = formatDate(startOfMonth);
 | 
				
			||||||
 | 
					    const endDate = formatDate(new Date(Date.now() + ONE_DAY));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [used, subs] = await Promise.all([
 | 
				
			||||||
 | 
					      fetch(
 | 
				
			||||||
 | 
					        this.path(
 | 
				
			||||||
 | 
					          `${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          method: "GET",
 | 
				
			||||||
 | 
					          headers: getHeaders(),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      fetch(this.path(OpenaiPath.SubsPath), {
 | 
				
			||||||
 | 
					        method: "GET",
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (used.status === 401) {
 | 
				
			||||||
 | 
					      throw new Error(Locale.Error.Unauthorized);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!used.ok || !subs.ok) {
 | 
				
			||||||
 | 
					      throw new Error("Failed to query usage from openai");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = (await used.json()) as {
 | 
				
			||||||
 | 
					      total_usage?: number;
 | 
				
			||||||
 | 
					      error?: {
 | 
				
			||||||
 | 
					        type: string;
 | 
				
			||||||
 | 
					        message: string;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const total = (await subs.json()) as {
 | 
				
			||||||
 | 
					      hard_limit_usd?: number;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response.error && response.error.type) {
 | 
				
			||||||
 | 
					      throw Error(response.error.message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response.total_usage) {
 | 
				
			||||||
 | 
					      response.total_usage = Math.round(response.total_usage) / 100;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (total.hard_limit_usd) {
 | 
				
			||||||
 | 
					      total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: response.total_usage,
 | 
				
			||||||
 | 
					      total: total.hard_limit_usd,
 | 
				
			||||||
 | 
					    } as LLMUsage;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    if (this.disableListModels) {
 | 
				
			||||||
 | 
					      return DEFAULT_MODELS.slice();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const res = await fetch(this.path(OpenaiPath.ListModelPath), {
 | 
				
			||||||
 | 
					      method: "GET",
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        ...getHeaders(),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const resJson = (await res.json()) as OpenAIListModelResponse;
 | 
				
			||||||
 | 
					    const chatModels = resJson.data?.filter(
 | 
				
			||||||
 | 
					      (m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    console.log("[Models]", chatModels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!chatModels) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场
 | 
				
			||||||
 | 
					    let seq = 1000; //同 Constant.ts 中的排序保持一致
 | 
				
			||||||
 | 
					    return chatModels.map((m) => ({
 | 
				
			||||||
 | 
					      name: m.id,
 | 
				
			||||||
 | 
					      available: true,
 | 
				
			||||||
 | 
					      sorted: seq++,
 | 
				
			||||||
 | 
					      provider: {
 | 
				
			||||||
 | 
					        id: "openai",
 | 
				
			||||||
 | 
					        providerName: "OpenAI",
 | 
				
			||||||
 | 
					        providerType: "openai",
 | 
				
			||||||
 | 
					        sorted: 1,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export { OpenaiPath };
 | 
				
			||||||
							
								
								
									
										279
									
								
								app/client/platforms/ppio.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								app/client/platforms/ppio.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,279 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import { ApiPath, PPIO_BASE_URL, PPIO, DEFAULT_MODELS } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { preProcessImageContent, streamWithThink } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					export interface PPIOListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class PPIOApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.ppioUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.PPIO;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? PPIO_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.PPIO)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      if (v.role === "assistant") {
 | 
				
			||||||
 | 
					        const content = getMessageTextContentWithoutThinking(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const content = visionModel
 | 
				
			||||||
 | 
					          ? await preProcessImageContent(v.content)
 | 
				
			||||||
 | 
					          : getMessageTextContent(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(PPIO.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // console.log(chatPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Use extended timeout for thinking models as they typically require more processing time
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string | null;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    if (this.disableListModels) {
 | 
				
			||||||
 | 
					      return DEFAULT_MODELS.slice();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const res = await fetch(this.path(PPIO.ListModelPath), {
 | 
				
			||||||
 | 
					      method: "GET",
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        ...getHeaders(),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const resJson = (await res.json()) as PPIOListModelResponse;
 | 
				
			||||||
 | 
					    const chatModels = resJson.data;
 | 
				
			||||||
 | 
					    console.log("[Models]", chatModels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!chatModels) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let seq = 1000;
 | 
				
			||||||
 | 
					    return chatModels.map((m) => ({
 | 
				
			||||||
 | 
					      name: m.id,
 | 
				
			||||||
 | 
					      available: true,
 | 
				
			||||||
 | 
					      sorted: seq++,
 | 
				
			||||||
 | 
					      provider: {
 | 
				
			||||||
 | 
					        id: "ppio",
 | 
				
			||||||
 | 
					        providerName: "PPIO",
 | 
				
			||||||
 | 
					        providerType: "ppio",
 | 
				
			||||||
 | 
					        sorted: 15,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										287
									
								
								app/client/platforms/siliconflow.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								app/client/platforms/siliconflow.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,287 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  SILICONFLOW_BASE_URL,
 | 
				
			||||||
 | 
					  SiliconFlow,
 | 
				
			||||||
 | 
					  DEFAULT_MODELS,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { preProcessImageContent, streamWithThink } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					export interface SiliconFlowListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SiliconflowApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.siliconflowUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.SiliconFlow;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? SILICONFLOW_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      !baseUrl.startsWith("http") &&
 | 
				
			||||||
 | 
					      !baseUrl.startsWith(ApiPath.SiliconFlow)
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      if (v.role === "assistant") {
 | 
				
			||||||
 | 
					        const content = getMessageTextContentWithoutThinking(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const content = visionModel
 | 
				
			||||||
 | 
					          ? await preProcessImageContent(v.content)
 | 
				
			||||||
 | 
					          : getMessageTextContent(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(SiliconFlow.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // console.log(chatPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Use extended timeout for thinking models as they typically require more processing time
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string | null;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    if (this.disableListModels) {
 | 
				
			||||||
 | 
					      return DEFAULT_MODELS.slice();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const res = await fetch(this.path(SiliconFlow.ListModelPath), {
 | 
				
			||||||
 | 
					      method: "GET",
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        ...getHeaders(),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const resJson = (await res.json()) as SiliconFlowListModelResponse;
 | 
				
			||||||
 | 
					    const chatModels = resJson.data;
 | 
				
			||||||
 | 
					    console.log("[Models]", chatModels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!chatModels) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let seq = 1000; //同 Constant.ts 中的排序保持一致
 | 
				
			||||||
 | 
					    return chatModels.map((m) => ({
 | 
				
			||||||
 | 
					      name: m.id,
 | 
				
			||||||
 | 
					      available: true,
 | 
				
			||||||
 | 
					      sorted: seq++,
 | 
				
			||||||
 | 
					      provider: {
 | 
				
			||||||
 | 
					        id: "siliconflow",
 | 
				
			||||||
 | 
					        providerName: "SiliconFlow",
 | 
				
			||||||
 | 
					        providerType: "siliconflow",
 | 
				
			||||||
 | 
					        sorted: 14,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										278
									
								
								app/client/platforms/tencent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								app/client/platforms/tencent.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,278 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, TENCENT_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import Locale from "../../locales";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  EventStreamContentType,
 | 
				
			||||||
 | 
					  fetchEventSource,
 | 
				
			||||||
 | 
					} from "@fortaine/fetch-event-source";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import mapKeys from "lodash-es/mapKeys";
 | 
				
			||||||
 | 
					import mapValues from "lodash-es/mapValues";
 | 
				
			||||||
 | 
					import isArray from "lodash-es/isArray";
 | 
				
			||||||
 | 
					import isObject from "lodash-es/isObject";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RequestPayload {
 | 
				
			||||||
 | 
					  Messages: {
 | 
				
			||||||
 | 
					    Role: "system" | "user" | "assistant";
 | 
				
			||||||
 | 
					    Content: string | MultimodalContent[];
 | 
				
			||||||
 | 
					  }[];
 | 
				
			||||||
 | 
					  Stream?: boolean;
 | 
				
			||||||
 | 
					  Model: string;
 | 
				
			||||||
 | 
					  Temperature: number;
 | 
				
			||||||
 | 
					  TopP: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function capitalizeKeys(obj: any): any {
 | 
				
			||||||
 | 
					  if (isArray(obj)) {
 | 
				
			||||||
 | 
					    return obj.map(capitalizeKeys);
 | 
				
			||||||
 | 
					  } else if (isObject(obj)) {
 | 
				
			||||||
 | 
					    return mapValues(
 | 
				
			||||||
 | 
					      mapKeys(obj, (value: any, key: string) =>
 | 
				
			||||||
 | 
					        key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      capitalizeKeys,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return obj;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class HunyuanApi implements LLMApi {
 | 
				
			||||||
 | 
					  path(): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.tencentUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl);
 | 
				
			||||||
 | 
					    return baseUrl;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.Choices?.at(0)?.Message?.Content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					    const messages = options.messages.map((v, index) => ({
 | 
				
			||||||
 | 
					      // "Messages 中 system 角色必须位于列表的最开始"
 | 
				
			||||||
 | 
					      role: index !== 0 && v.role === "system" ? "user" : v.role,
 | 
				
			||||||
 | 
					      content: visionModel ? v.content : getMessageTextContent(v),
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = capitalizeKeys({
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] Tencent payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path();
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        let responseText = "";
 | 
				
			||||||
 | 
					        let remainText = "";
 | 
				
			||||||
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        let responseRes: Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // animate response to make it looks smooth
 | 
				
			||||||
 | 
					        function animateResponseText() {
 | 
				
			||||||
 | 
					          if (finished || controller.signal.aborted) {
 | 
				
			||||||
 | 
					            responseText += remainText;
 | 
				
			||||||
 | 
					            console.log("[Response Animation] finished");
 | 
				
			||||||
 | 
					            if (responseText?.length === 0) {
 | 
				
			||||||
 | 
					              options.onError?.(new Error("empty response from server"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (remainText.length > 0) {
 | 
				
			||||||
 | 
					            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
				
			||||||
 | 
					            const fetchText = remainText.slice(0, fetchCount);
 | 
				
			||||||
 | 
					            responseText += fetchText;
 | 
				
			||||||
 | 
					            remainText = remainText.slice(fetchCount);
 | 
				
			||||||
 | 
					            options.onUpdate?.(responseText, fetchText);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          requestAnimationFrame(animateResponseText);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // start animaion
 | 
				
			||||||
 | 
					        animateResponseText();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const finish = () => {
 | 
				
			||||||
 | 
					          if (!finished) {
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					            options.onFinish(responseText + remainText, responseRes);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          fetch: fetch as any,
 | 
				
			||||||
 | 
					          ...chatPayload,
 | 
				
			||||||
 | 
					          async onopen(res) {
 | 
				
			||||||
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					            const contentType = res.headers.get("content-type");
 | 
				
			||||||
 | 
					            console.log(
 | 
				
			||||||
 | 
					              "[Tencent] request response content type: ",
 | 
				
			||||||
 | 
					              contentType,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            responseRes = res;
 | 
				
			||||||
 | 
					            if (contentType?.startsWith("text/plain")) {
 | 
				
			||||||
 | 
					              responseText = await res.clone().text();
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              !res.ok ||
 | 
				
			||||||
 | 
					              !res.headers
 | 
				
			||||||
 | 
					                .get("content-type")
 | 
				
			||||||
 | 
					                ?.startsWith(EventStreamContentType) ||
 | 
				
			||||||
 | 
					              res.status !== 200
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              const responseTexts = [responseText];
 | 
				
			||||||
 | 
					              let extraInfo = await res.clone().text();
 | 
				
			||||||
 | 
					              try {
 | 
				
			||||||
 | 
					                const resJson = await res.clone().json();
 | 
				
			||||||
 | 
					                extraInfo = prettyObject(resJson);
 | 
				
			||||||
 | 
					              } catch {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (res.status === 401) {
 | 
				
			||||||
 | 
					                responseTexts.push(Locale.Error.Unauthorized);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (extraInfo) {
 | 
				
			||||||
 | 
					                responseTexts.push(extraInfo);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              responseText = responseTexts.join("\n\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onmessage(msg) {
 | 
				
			||||||
 | 
					            if (msg.data === "[DONE]" || finished) {
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const text = msg.data;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              const json = JSON.parse(text);
 | 
				
			||||||
 | 
					              const choices = json.Choices as Array<{
 | 
				
			||||||
 | 
					                Delta: { Content: string };
 | 
				
			||||||
 | 
					              }>;
 | 
				
			||||||
 | 
					              const delta = choices[0]?.Delta?.Content;
 | 
				
			||||||
 | 
					              if (delta) {
 | 
				
			||||||
 | 
					                remainText += delta;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					              console.error("[Request] parse error", text, msg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onclose() {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onerror(e) {
 | 
				
			||||||
 | 
					            options.onError?.(e);
 | 
				
			||||||
 | 
					            throw e;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          openWhenHidden: true,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										194
									
								
								app/client/platforms/xai.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								app/client/platforms/xai.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,194 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import { ApiPath, XAI_BASE_URL, XAI } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { stream } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { getTimeoutMSByModel } from "@/app/utils";
 | 
				
			||||||
 | 
					import { preProcessImageContent } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class XAIApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.xaiUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.XAI;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? XAI_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = await preProcessImageContent(v.content);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] xai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(XAI.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return stream(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										79
									
								
								app/command.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								app/command.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					import { useSearchParams } from "react-router-dom";
 | 
				
			||||||
 | 
					import Locale from "./locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Command = (param: string) => void;
 | 
				
			||||||
 | 
					interface Commands {
 | 
				
			||||||
 | 
					  fill?: Command;
 | 
				
			||||||
 | 
					  submit?: Command;
 | 
				
			||||||
 | 
					  mask?: Command;
 | 
				
			||||||
 | 
					  code?: Command;
 | 
				
			||||||
 | 
					  settings?: Command;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useCommand(commands: Commands = {}) {
 | 
				
			||||||
 | 
					  const [searchParams, setSearchParams] = useSearchParams();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    let shouldUpdate = false;
 | 
				
			||||||
 | 
					    searchParams.forEach((param, name) => {
 | 
				
			||||||
 | 
					      const commandName = name as keyof Commands;
 | 
				
			||||||
 | 
					      if (typeof commands[commandName] === "function") {
 | 
				
			||||||
 | 
					        commands[commandName]!(param);
 | 
				
			||||||
 | 
					        searchParams.delete(name);
 | 
				
			||||||
 | 
					        shouldUpdate = true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (shouldUpdate) {
 | 
				
			||||||
 | 
					      setSearchParams(searchParams);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [searchParams, commands]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChatCommands {
 | 
				
			||||||
 | 
					  new?: Command;
 | 
				
			||||||
 | 
					  newm?: Command;
 | 
				
			||||||
 | 
					  next?: Command;
 | 
				
			||||||
 | 
					  prev?: Command;
 | 
				
			||||||
 | 
					  clear?: Command;
 | 
				
			||||||
 | 
					  fork?: Command;
 | 
				
			||||||
 | 
					  del?: Command;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Compatible with Chinese colon character ":"
 | 
				
			||||||
 | 
					export const ChatCommandPrefix = /^[::]/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useChatCommand(commands: ChatCommands = {}) {
 | 
				
			||||||
 | 
					  function extract(userInput: string) {
 | 
				
			||||||
 | 
					    const match = userInput.match(ChatCommandPrefix);
 | 
				
			||||||
 | 
					    if (match) {
 | 
				
			||||||
 | 
					      return userInput.slice(1) as keyof ChatCommands;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return userInput as keyof ChatCommands;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function search(userInput: string) {
 | 
				
			||||||
 | 
					    const input = extract(userInput);
 | 
				
			||||||
 | 
					    const desc = Locale.Chat.Commands;
 | 
				
			||||||
 | 
					    return Object.keys(commands)
 | 
				
			||||||
 | 
					      .filter((c) => c.startsWith(input))
 | 
				
			||||||
 | 
					      .map((c) => ({
 | 
				
			||||||
 | 
					        title: desc[c as keyof ChatCommands],
 | 
				
			||||||
 | 
					        content: ":" + c,
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function match(userInput: string) {
 | 
				
			||||||
 | 
					    const command = extract(userInput);
 | 
				
			||||||
 | 
					    const matched = typeof commands[command] === "function";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      matched,
 | 
				
			||||||
 | 
					      invoke: () => matched && commands[command]!(userInput),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return { match, search };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								app/components/artifacts.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/components/artifacts.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					.artifacts {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  &-header {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    height: 36px;
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    background: var(--second);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  &-title {
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    font-size: 24px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  &-content {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    padding: 0 20px 20px 20px;
 | 
				
			||||||
 | 
					    background-color: var(--second);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.artifacts-iframe {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  background-color: var(--gray);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										266
									
								
								app/components/artifacts.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								app/components/artifacts.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,266 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useEffect,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					  useRef,
 | 
				
			||||||
 | 
					  useMemo,
 | 
				
			||||||
 | 
					  forwardRef,
 | 
				
			||||||
 | 
					  useImperativeHandle,
 | 
				
			||||||
 | 
					} from "react";
 | 
				
			||||||
 | 
					import { useParams } from "react-router";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { nanoid } from "nanoid";
 | 
				
			||||||
 | 
					import ExportIcon from "../icons/share.svg";
 | 
				
			||||||
 | 
					import CopyIcon from "../icons/copy.svg";
 | 
				
			||||||
 | 
					import DownloadIcon from "../icons/download.svg";
 | 
				
			||||||
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
 | 
					import LoadingButtonIcon from "../icons/loading.svg";
 | 
				
			||||||
 | 
					import ReloadButtonIcon from "../icons/reload.svg";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					import { Modal, showToast } from "./ui-lib";
 | 
				
			||||||
 | 
					import { copyToClipboard, downloadAs } from "../utils";
 | 
				
			||||||
 | 
					import { Path, ApiPath, REPO_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { Loading } from "./home";
 | 
				
			||||||
 | 
					import styles from "./artifacts.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HTMLPreviewProps = {
 | 
				
			||||||
 | 
					  code: string;
 | 
				
			||||||
 | 
					  autoHeight?: boolean;
 | 
				
			||||||
 | 
					  height?: number | string;
 | 
				
			||||||
 | 
					  onLoad?: (title?: string) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type HTMLPreviewHander = {
 | 
				
			||||||
 | 
					  reload: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
 | 
				
			||||||
 | 
					  function HTMLPreview(props, ref) {
 | 
				
			||||||
 | 
					    const iframeRef = useRef<HTMLIFrameElement>(null);
 | 
				
			||||||
 | 
					    const [frameId, setFrameId] = useState<string>(nanoid());
 | 
				
			||||||
 | 
					    const [iframeHeight, setIframeHeight] = useState(600);
 | 
				
			||||||
 | 
					    const [title, setTitle] = useState("");
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
 | 
					     * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
 | 
				
			||||||
 | 
					     * 1. using srcdoc
 | 
				
			||||||
 | 
					     * 2. using src with dataurl:
 | 
				
			||||||
 | 
					     *    easy to share
 | 
				
			||||||
 | 
					     *    length limit (Data URIs cannot be larger than 32,768 characters.)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      const handleMessage = (e: any) => {
 | 
				
			||||||
 | 
					        const { id, height, title } = e.data;
 | 
				
			||||||
 | 
					        setTitle(title);
 | 
				
			||||||
 | 
					        if (id == frameId) {
 | 
				
			||||||
 | 
					          setIframeHeight(height);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      window.addEventListener("message", handleMessage);
 | 
				
			||||||
 | 
					      return () => {
 | 
				
			||||||
 | 
					        window.removeEventListener("message", handleMessage);
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }, [frameId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useImperativeHandle(ref, () => ({
 | 
				
			||||||
 | 
					      reload: () => {
 | 
				
			||||||
 | 
					        setFrameId(nanoid());
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const height = useMemo(() => {
 | 
				
			||||||
 | 
					      if (!props.autoHeight) return props.height || 600;
 | 
				
			||||||
 | 
					      if (typeof props.height === "string") {
 | 
				
			||||||
 | 
					        return props.height;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const parentHeight = props.height || 600;
 | 
				
			||||||
 | 
					      return iframeHeight + 40 > parentHeight
 | 
				
			||||||
 | 
					        ? parentHeight
 | 
				
			||||||
 | 
					        : iframeHeight + 40;
 | 
				
			||||||
 | 
					    }, [props.autoHeight, props.height, iframeHeight]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const srcDoc = useMemo(() => {
 | 
				
			||||||
 | 
					      const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
 | 
				
			||||||
 | 
					      if (props.code.includes("<!DOCTYPE html>")) {
 | 
				
			||||||
 | 
					        props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return script + props.code;
 | 
				
			||||||
 | 
					    }, [props.code, frameId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleOnLoad = () => {
 | 
				
			||||||
 | 
					      if (props?.onLoad) {
 | 
				
			||||||
 | 
					        props.onLoad(title);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <iframe
 | 
				
			||||||
 | 
					        className={styles["artifacts-iframe"]}
 | 
				
			||||||
 | 
					        key={frameId}
 | 
				
			||||||
 | 
					        ref={iframeRef}
 | 
				
			||||||
 | 
					        sandbox="allow-forms allow-modals allow-scripts"
 | 
				
			||||||
 | 
					        style={{ height }}
 | 
				
			||||||
 | 
					        srcDoc={srcDoc}
 | 
				
			||||||
 | 
					        onLoad={handleOnLoad}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ArtifactsShareButton({
 | 
				
			||||||
 | 
					  getCode,
 | 
				
			||||||
 | 
					  id,
 | 
				
			||||||
 | 
					  style,
 | 
				
			||||||
 | 
					  fileName,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  getCode: () => string;
 | 
				
			||||||
 | 
					  id?: string;
 | 
				
			||||||
 | 
					  style?: any;
 | 
				
			||||||
 | 
					  fileName?: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					  const [name, setName] = useState(id);
 | 
				
			||||||
 | 
					  const [show, setShow] = useState(false);
 | 
				
			||||||
 | 
					  const shareUrl = useMemo(
 | 
				
			||||||
 | 
					    () => [location.origin, "#", Path.Artifacts, "/", name].join(""),
 | 
				
			||||||
 | 
					    [name],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const upload = (code: string) =>
 | 
				
			||||||
 | 
					    id
 | 
				
			||||||
 | 
					      ? Promise.resolve({ id })
 | 
				
			||||||
 | 
					      : fetch(ApiPath.Artifacts, {
 | 
				
			||||||
 | 
					          method: "POST",
 | 
				
			||||||
 | 
					          body: code,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					          .then((res) => res.json())
 | 
				
			||||||
 | 
					          .then(({ id }) => {
 | 
				
			||||||
 | 
					            if (id) {
 | 
				
			||||||
 | 
					              return { id };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            throw Error();
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch((e) => {
 | 
				
			||||||
 | 
					            showToast(Locale.Export.Artifacts.Error);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div className="window-action-button" style={style}>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          icon={loading ? <LoadingButtonIcon /> : <ExportIcon />}
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					          title={Locale.Export.Artifacts.Title}
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            if (loading) return;
 | 
				
			||||||
 | 
					            setLoading(true);
 | 
				
			||||||
 | 
					            upload(getCode())
 | 
				
			||||||
 | 
					              .then((res) => {
 | 
				
			||||||
 | 
					                if (res?.id) {
 | 
				
			||||||
 | 
					                  setShow(true);
 | 
				
			||||||
 | 
					                  setName(res?.id);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					              .finally(() => setLoading(false));
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {show && (
 | 
				
			||||||
 | 
					        <div className="modal-mask">
 | 
				
			||||||
 | 
					          <Modal
 | 
				
			||||||
 | 
					            title={Locale.Export.Artifacts.Title}
 | 
				
			||||||
 | 
					            onClose={() => setShow(false)}
 | 
				
			||||||
 | 
					            actions={[
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                key="download"
 | 
				
			||||||
 | 
					                icon={<DownloadIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                text={Locale.Export.Download}
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  downloadAs(getCode(), `${fileName || name}.html`).then(() =>
 | 
				
			||||||
 | 
					                    setShow(false),
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />,
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                key="copy"
 | 
				
			||||||
 | 
					                icon={<CopyIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                text={Locale.Chat.Actions.Copy}
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  copyToClipboard(shareUrl).then(() => setShow(false));
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />,
 | 
				
			||||||
 | 
					            ]}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <a target="_blank" href={shareUrl}>
 | 
				
			||||||
 | 
					                {shareUrl}
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Modal>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Artifacts() {
 | 
				
			||||||
 | 
					  const { id } = useParams();
 | 
				
			||||||
 | 
					  const [code, setCode] = useState("");
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(true);
 | 
				
			||||||
 | 
					  const [fileName, setFileName] = useState("");
 | 
				
			||||||
 | 
					  const previewRef = useRef<HTMLPreviewHander>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (id) {
 | 
				
			||||||
 | 
					      fetch(`${ApiPath.Artifacts}?id=${id}`)
 | 
				
			||||||
 | 
					        .then((res) => {
 | 
				
			||||||
 | 
					          if (res.status > 300) {
 | 
				
			||||||
 | 
					            throw Error("can not get content");
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return res;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then((res) => res.text())
 | 
				
			||||||
 | 
					        .then(setCode)
 | 
				
			||||||
 | 
					        .catch((e) => {
 | 
				
			||||||
 | 
					          showToast(Locale.Export.Artifacts.Error);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["artifacts"]}>
 | 
				
			||||||
 | 
					      <div className={styles["artifacts-header"]}>
 | 
				
			||||||
 | 
					        <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
 | 
					          <IconButton bordered icon={<GithubIcon />} shadow />
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					          style={{ marginLeft: 20 }}
 | 
				
			||||||
 | 
					          icon={<ReloadButtonIcon />}
 | 
				
			||||||
 | 
					          shadow
 | 
				
			||||||
 | 
					          onClick={() => previewRef.current?.reload()}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <div className={styles["artifacts-title"]}>NextChat Artifacts</div>
 | 
				
			||||||
 | 
					        <ArtifactsShareButton
 | 
				
			||||||
 | 
					          id={id}
 | 
				
			||||||
 | 
					          getCode={() => code}
 | 
				
			||||||
 | 
					          fileName={fileName}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className={styles["artifacts-content"]}>
 | 
				
			||||||
 | 
					        {loading && <Loading />}
 | 
				
			||||||
 | 
					        {code && (
 | 
				
			||||||
 | 
					          <HTMLPreview
 | 
				
			||||||
 | 
					            code={code}
 | 
				
			||||||
 | 
					            ref={previewRef}
 | 
				
			||||||
 | 
					            autoHeight={false}
 | 
				
			||||||
 | 
					            height={"100%"}
 | 
				
			||||||
 | 
					            onLoad={(title) => {
 | 
				
			||||||
 | 
					              setFileName(title as string);
 | 
				
			||||||
 | 
					              setLoading(false);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										99
									
								
								app/components/auth.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								app/components/auth.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					.auth-page {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: flex-start;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  .top-banner {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    padding: 12px 64px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    background: var(--second);
 | 
				
			||||||
 | 
					    .top-banner-inner {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      line-height: 150%;
 | 
				
			||||||
 | 
					      span {
 | 
				
			||||||
 | 
					        gap: 8px;
 | 
				
			||||||
 | 
					        a {
 | 
				
			||||||
 | 
					          display: inline-flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          text-decoration: none;
 | 
				
			||||||
 | 
					          margin-left: 8px;
 | 
				
			||||||
 | 
					          color: var(--primary);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .top-banner-close {
 | 
				
			||||||
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					      position: absolute;
 | 
				
			||||||
 | 
					      top: 50%;
 | 
				
			||||||
 | 
					      right: 48px;
 | 
				
			||||||
 | 
					      transform: translateY(-50%);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (max-width: 600px) {
 | 
				
			||||||
 | 
					    .top-banner {
 | 
				
			||||||
 | 
					      padding: 12px 24px 12px 12px;
 | 
				
			||||||
 | 
					      .top-banner-close {
 | 
				
			||||||
 | 
					        right: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .top-banner-inner {
 | 
				
			||||||
 | 
					        .top-banner-logo {
 | 
				
			||||||
 | 
					          margin-right: 8px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-header {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    animation: slide-in-from-top ease 0.3s;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-logo {
 | 
				
			||||||
 | 
					    margin-top: 10vh;
 | 
				
			||||||
 | 
					    transform: scale(1.4);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-title {
 | 
				
			||||||
 | 
					    font-size: 24px;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    line-height: 2;
 | 
				
			||||||
 | 
					    margin-bottom: 1vh;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-tips {
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-input {
 | 
				
			||||||
 | 
					    margin: 3vh 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-input-second {
 | 
				
			||||||
 | 
					    margin: 0 0 3vh 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-actions {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    button:not(:last-child) {
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										189
									
								
								app/components/auth.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								app/components/auth.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
				
			|||||||
 | 
					import styles from "./auth.module.scss";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { useState, useEffect } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { Path, SAAS_CHAT_URL } from "../constant";
 | 
				
			||||||
 | 
					import { useAccessStore } from "../store";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					import Delete from "../icons/close.svg";
 | 
				
			||||||
 | 
					import Arrow from "../icons/arrow.svg";
 | 
				
			||||||
 | 
					import Logo from "../icons/logo.svg";
 | 
				
			||||||
 | 
					import { useMobileScreen } from "@/app/utils";
 | 
				
			||||||
 | 
					import BotIcon from "../icons/bot.svg";
 | 
				
			||||||
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
 | 
					import { PasswordInput } from "./ui-lib";
 | 
				
			||||||
 | 
					import LeftIcon from "@/app/icons/left.svg";
 | 
				
			||||||
 | 
					import { safeLocalStorage } from "@/app/utils";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  trackSettingsPageGuideToCPaymentClick,
 | 
				
			||||||
 | 
					  trackAuthorizationPageButtonToCPaymentClick,
 | 
				
			||||||
 | 
					} from "../utils/auth-settings-events";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const storage = safeLocalStorage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function AuthPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const accessStore = useAccessStore();
 | 
				
			||||||
 | 
					  const goHome = () => navigate(Path.Home);
 | 
				
			||||||
 | 
					  const goChat = () => navigate(Path.Chat);
 | 
				
			||||||
 | 
					  const goSaas = () => {
 | 
				
			||||||
 | 
					    trackAuthorizationPageButtonToCPaymentClick();
 | 
				
			||||||
 | 
					    window.location.href = SAAS_CHAT_URL;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const resetAccessCode = () => {
 | 
				
			||||||
 | 
					    accessStore.update((access) => {
 | 
				
			||||||
 | 
					      access.openaiApiKey = "";
 | 
				
			||||||
 | 
					      access.accessCode = "";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }; // Reset access code to empty string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (getClientConfig()?.isApp) {
 | 
				
			||||||
 | 
					      navigate(Path.Settings);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["auth-page"]}>
 | 
				
			||||||
 | 
					      <TopBanner></TopBanner>
 | 
				
			||||||
 | 
					      <div className={styles["auth-header"]}>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          icon={<LeftIcon />}
 | 
				
			||||||
 | 
					          text={Locale.Auth.Return}
 | 
				
			||||||
 | 
					          onClick={() => navigate(Path.Home)}
 | 
				
			||||||
 | 
					        ></IconButton>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className={clsx("no-dark", styles["auth-logo"])}>
 | 
				
			||||||
 | 
					        <BotIcon />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
 | 
				
			||||||
 | 
					      <div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <PasswordInput
 | 
				
			||||||
 | 
					        style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
				
			||||||
 | 
					        aria={Locale.Settings.ShowPassword}
 | 
				
			||||||
 | 
					        aria-label={Locale.Auth.Input}
 | 
				
			||||||
 | 
					        value={accessStore.accessCode}
 | 
				
			||||||
 | 
					        type="text"
 | 
				
			||||||
 | 
					        placeholder={Locale.Auth.Input}
 | 
				
			||||||
 | 
					        onChange={(e) => {
 | 
				
			||||||
 | 
					          accessStore.update(
 | 
				
			||||||
 | 
					            (access) => (access.accessCode = e.currentTarget.value),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {!accessStore.hideUserApiKey ? (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
 | 
				
			||||||
 | 
					          <PasswordInput
 | 
				
			||||||
 | 
					            style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
				
			||||||
 | 
					            aria={Locale.Settings.ShowPassword}
 | 
				
			||||||
 | 
					            aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					            value={accessStore.openaiApiKey}
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              accessStore.update(
 | 
				
			||||||
 | 
					                (access) => (access.openaiApiKey = e.currentTarget.value),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <PasswordInput
 | 
				
			||||||
 | 
					            style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
				
			||||||
 | 
					            aria={Locale.Settings.ShowPassword}
 | 
				
			||||||
 | 
					            aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					            value={accessStore.googleApiKey}
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              accessStore.update(
 | 
				
			||||||
 | 
					                (access) => (access.googleApiKey = e.currentTarget.value),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className={styles["auth-actions"]}>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          text={Locale.Auth.Confirm}
 | 
				
			||||||
 | 
					          type="primary"
 | 
				
			||||||
 | 
					          onClick={goChat}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          text={Locale.Auth.SaasTips}
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            goSaas();
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function TopBanner() {
 | 
				
			||||||
 | 
					  const [isHovered, setIsHovered] = useState(false);
 | 
				
			||||||
 | 
					  const [isVisible, setIsVisible] = useState(true);
 | 
				
			||||||
 | 
					  const isMobile = useMobileScreen();
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // 检查 localStorage 中是否有标记
 | 
				
			||||||
 | 
					    const bannerDismissed = storage.getItem("bannerDismissed");
 | 
				
			||||||
 | 
					    // 如果标记不存在,存储默认值并显示横幅
 | 
				
			||||||
 | 
					    if (!bannerDismissed) {
 | 
				
			||||||
 | 
					      storage.setItem("bannerDismissed", "false");
 | 
				
			||||||
 | 
					      setIsVisible(true); // 显示横幅
 | 
				
			||||||
 | 
					    } else if (bannerDismissed === "true") {
 | 
				
			||||||
 | 
					      // 如果标记为 "true",则隐藏横幅
 | 
				
			||||||
 | 
					      setIsVisible(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleMouseEnter = () => {
 | 
				
			||||||
 | 
					    setIsHovered(true);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleMouseLeave = () => {
 | 
				
			||||||
 | 
					    setIsHovered(false);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClose = () => {
 | 
				
			||||||
 | 
					    setIsVisible(false);
 | 
				
			||||||
 | 
					    storage.setItem("bannerDismissed", "true");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!isVisible) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={styles["top-banner"]}
 | 
				
			||||||
 | 
					      onMouseEnter={handleMouseEnter}
 | 
				
			||||||
 | 
					      onMouseLeave={handleMouseLeave}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div className={clsx(styles["top-banner-inner"], "no-dark")}>
 | 
				
			||||||
 | 
					        <Logo className={styles["top-banner-logo"]}></Logo>
 | 
				
			||||||
 | 
					        <span>
 | 
				
			||||||
 | 
					          {Locale.Auth.TopTips}
 | 
				
			||||||
 | 
					          <a
 | 
				
			||||||
 | 
					            href={SAAS_CHAT_URL}
 | 
				
			||||||
 | 
					            rel="stylesheet"
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              trackSettingsPageGuideToCPaymentClick();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {Locale.Settings.Access.SaasStart.ChatNow}
 | 
				
			||||||
 | 
					            <Arrow style={{ marginLeft: "4px" }} />
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {(isHovered || isMobile) && (
 | 
				
			||||||
 | 
					        <Delete className={styles["top-banner-close"]} onClick={handleClose} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,7 +5,6 @@
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  padding: 10px;
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: all 0.3s ease;
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
@@ -27,6 +26,26 @@
 | 
				
			|||||||
      fill: white !important;
 | 
					      fill: white !important;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.danger {
 | 
				
			||||||
 | 
					    color: rgba($color: red, $alpha: 0.8);
 | 
				
			||||||
 | 
					    border-color: rgba($color: red, $alpha: 0.5);
 | 
				
			||||||
 | 
					    background-color: rgba($color: red, $alpha: 0.05);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      border-color: red;
 | 
				
			||||||
 | 
					      background-color: rgba($color: red, $alpha: 0.1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    path {
 | 
				
			||||||
 | 
					      fill: red !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover,
 | 
				
			||||||
 | 
					  &:focus {
 | 
				
			||||||
 | 
					    border-color: var(--primary);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.shadow {
 | 
					.shadow {
 | 
				
			||||||
@@ -37,10 +56,6 @@
 | 
				
			|||||||
  border: var(--border-in-light);
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.icon-button:hover {
 | 
					 | 
				
			||||||
  border-color: var(--primary);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.icon-button-icon {
 | 
					.icon-button-icon {
 | 
				
			||||||
  width: 16px;
 | 
					  width: 16px;
 | 
				
			||||||
  height: 16px;
 | 
					  height: 16px;
 | 
				
			||||||
@@ -56,9 +71,12 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.icon-button-text {
 | 
					.icon-button-text {
 | 
				
			||||||
  margin-left: 5px;
 | 
					 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
  text-overflow: ellipsis;
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
  white-space: nowrap;
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:not(:first-child) {
 | 
				
			||||||
 | 
					    margin-left: 5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,44 +1,65 @@
 | 
				
			|||||||
import * as React from "react";
 | 
					import * as React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./button.module.scss";
 | 
					import styles from "./button.module.scss";
 | 
				
			||||||
 | 
					import { CSSProperties } from "react";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ButtonType = "primary" | "danger" | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function IconButton(props: {
 | 
					export function IconButton(props: {
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
  icon?: JSX.Element;
 | 
					  icon?: JSX.Element;
 | 
				
			||||||
  type?: "primary" | "danger";
 | 
					  type?: ButtonType;
 | 
				
			||||||
  text?: string;
 | 
					  text?: string;
 | 
				
			||||||
  bordered?: boolean;
 | 
					  bordered?: boolean;
 | 
				
			||||||
  shadow?: boolean;
 | 
					  shadow?: boolean;
 | 
				
			||||||
  className?: string;
 | 
					  className?: string;
 | 
				
			||||||
  title?: string;
 | 
					  title?: string;
 | 
				
			||||||
  disabled?: boolean;
 | 
					  disabled?: boolean;
 | 
				
			||||||
 | 
					  tabIndex?: number;
 | 
				
			||||||
 | 
					  autoFocus?: boolean;
 | 
				
			||||||
 | 
					  style?: CSSProperties;
 | 
				
			||||||
 | 
					  aria?: string;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <button
 | 
					    <button
 | 
				
			||||||
      className={
 | 
					      className={clsx(
 | 
				
			||||||
        styles["icon-button"] +
 | 
					        "clickable",
 | 
				
			||||||
        ` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
 | 
					        styles["icon-button"],
 | 
				
			||||||
          props.className ?? ""
 | 
					        {
 | 
				
			||||||
        } clickable ${styles[props.type ?? ""]}`
 | 
					          [styles.border]: props.bordered,
 | 
				
			||||||
      }
 | 
					          [styles.shadow]: props.shadow,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        styles[props.type ?? ""],
 | 
				
			||||||
 | 
					        props.className,
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      onClick={props.onClick}
 | 
					      onClick={props.onClick}
 | 
				
			||||||
      title={props.title}
 | 
					      title={props.title}
 | 
				
			||||||
      disabled={props.disabled}
 | 
					      disabled={props.disabled}
 | 
				
			||||||
      role="button"
 | 
					      role="button"
 | 
				
			||||||
 | 
					      tabIndex={props.tabIndex}
 | 
				
			||||||
 | 
					      autoFocus={props.autoFocus}
 | 
				
			||||||
 | 
					      style={props.style}
 | 
				
			||||||
 | 
					      aria-label={props.aria}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      {props.icon && (
 | 
					      {props.icon && (
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          className={
 | 
					          aria-label={props.text || props.title}
 | 
				
			||||||
            styles["icon-button-icon"] +
 | 
					          className={clsx(styles["icon-button-icon"], {
 | 
				
			||||||
            ` ${props.type === "primary" && "no-dark"}`
 | 
					            "no-dark": props.type === "primary",
 | 
				
			||||||
          }
 | 
					          })}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {props.icon}
 | 
					          {props.icon}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {props.text && (
 | 
					      {props.text && (
 | 
				
			||||||
        <div className={styles["icon-button-text"]}>{props.text}</div>
 | 
					        <div
 | 
				
			||||||
 | 
					          aria-label={props.text || props.title}
 | 
				
			||||||
 | 
					          className={styles["icon-button-text"]}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {props.text}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
import DeleteIcon from "../icons/delete.svg";
 | 
					import DeleteIcon from "../icons/delete.svg";
 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./home.module.scss";
 | 
					import styles from "./home.module.scss";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -12,10 +11,14 @@ import {
 | 
				
			|||||||
import { useChatStore } from "../store";
 | 
					import { useChatStore } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { Link, useNavigate } from "react-router-dom";
 | 
					import { useLocation, useNavigate } from "react-router-dom";
 | 
				
			||||||
import { Path } from "../constant";
 | 
					import { Path } from "../constant";
 | 
				
			||||||
import { MaskAvatar } from "./mask";
 | 
					import { MaskAvatar } from "./mask";
 | 
				
			||||||
import { Mask } from "../store/mask";
 | 
					import { Mask } from "../store/mask";
 | 
				
			||||||
 | 
					import { useRef, useEffect } from "react";
 | 
				
			||||||
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
 | 
					import { useMobileScreen } from "../utils";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ChatItem(props: {
 | 
					export function ChatItem(props: {
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
@@ -24,20 +27,35 @@ export function ChatItem(props: {
 | 
				
			|||||||
  count: number;
 | 
					  count: number;
 | 
				
			||||||
  time: string;
 | 
					  time: string;
 | 
				
			||||||
  selected: boolean;
 | 
					  selected: boolean;
 | 
				
			||||||
  id: number;
 | 
					  id: string;
 | 
				
			||||||
  index: number;
 | 
					  index: number;
 | 
				
			||||||
  narrow?: boolean;
 | 
					  narrow?: boolean;
 | 
				
			||||||
  mask: Mask;
 | 
					  mask: Mask;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
 | 
					  const draggableRef = useRef<HTMLDivElement | null>(null);
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (props.selected && draggableRef.current) {
 | 
				
			||||||
 | 
					      draggableRef.current?.scrollIntoView({
 | 
				
			||||||
 | 
					        block: "center",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [props.selected]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { pathname: currentPath } = useLocation();
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Draggable draggableId={`${props.id}`} index={props.index}>
 | 
					    <Draggable draggableId={`${props.id}`} index={props.index}>
 | 
				
			||||||
      {(provided) => (
 | 
					      {(provided) => (
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          className={`${styles["chat-item"]} ${
 | 
					          className={clsx(styles["chat-item"], {
 | 
				
			||||||
            props.selected && styles["chat-item-selected"]
 | 
					            [styles["chat-item-selected"]]:
 | 
				
			||||||
          }`}
 | 
					              props.selected &&
 | 
				
			||||||
 | 
					              (currentPath === Path.Chat || currentPath === Path.Home),
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
          onClick={props.onClick}
 | 
					          onClick={props.onClick}
 | 
				
			||||||
          ref={provided.innerRef}
 | 
					          ref={(ele) => {
 | 
				
			||||||
 | 
					            draggableRef.current = ele;
 | 
				
			||||||
 | 
					            provided.innerRef(ele);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
          {...provided.draggableProps}
 | 
					          {...provided.draggableProps}
 | 
				
			||||||
          {...provided.dragHandleProps}
 | 
					          {...provided.dragHandleProps}
 | 
				
			||||||
          title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
 | 
					          title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
 | 
				
			||||||
@@ -46,8 +64,11 @@ export function ChatItem(props: {
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          {props.narrow ? (
 | 
					          {props.narrow ? (
 | 
				
			||||||
            <div className={styles["chat-item-narrow"]}>
 | 
					            <div className={styles["chat-item-narrow"]}>
 | 
				
			||||||
              <div className={styles["chat-item-avatar"] + " no-dark"}>
 | 
					              <div className={clsx(styles["chat-item-avatar"], "no-dark")}>
 | 
				
			||||||
                <MaskAvatar mask={props.mask} />
 | 
					                <MaskAvatar
 | 
				
			||||||
 | 
					                  avatar={props.mask.avatar}
 | 
				
			||||||
 | 
					                  model={props.mask.modelConfig.model}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div className={styles["chat-item-narrow-count"]}>
 | 
					              <div className={styles["chat-item-narrow-count"]}>
 | 
				
			||||||
                {props.count}
 | 
					                {props.count}
 | 
				
			||||||
@@ -60,14 +81,19 @@ export function ChatItem(props: {
 | 
				
			|||||||
                <div className={styles["chat-item-count"]}>
 | 
					                <div className={styles["chat-item-count"]}>
 | 
				
			||||||
                  {Locale.ChatItem.ChatItemCount(props.count)}
 | 
					                  {Locale.ChatItem.ChatItemCount(props.count)}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className={styles["chat-item-date"]}>
 | 
					                <div className={styles["chat-item-date"]}>{props.time}</div>
 | 
				
			||||||
                  {new Date(props.time).toLocaleString()}
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </>
 | 
					            </>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div className={styles["chat-item-delete"]} onClick={props.onDelete}>
 | 
					          <div
 | 
				
			||||||
 | 
					            className={styles["chat-item-delete"]}
 | 
				
			||||||
 | 
					            onClickCapture={(e) => {
 | 
				
			||||||
 | 
					              props.onDelete?.();
 | 
				
			||||||
 | 
					              e.preventDefault();
 | 
				
			||||||
 | 
					              e.stopPropagation();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            <DeleteIcon />
 | 
					            <DeleteIcon />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@@ -77,16 +103,17 @@ export function ChatItem(props: {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ChatList(props: { narrow?: boolean }) {
 | 
					export function ChatList(props: { narrow?: boolean }) {
 | 
				
			||||||
  const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
 | 
					  const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
 | 
				
			||||||
    useChatStore((state) => [
 | 
					    (state) => [
 | 
				
			||||||
      state.sessions,
 | 
					      state.sessions,
 | 
				
			||||||
      state.currentSessionIndex,
 | 
					      state.currentSessionIndex,
 | 
				
			||||||
      state.selectSession,
 | 
					      state.selectSession,
 | 
				
			||||||
      state.removeSession,
 | 
					 | 
				
			||||||
      state.moveSession,
 | 
					      state.moveSession,
 | 
				
			||||||
    ]);
 | 
					    ],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const isMobileScreen = useMobileScreen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onDragEnd: OnDragEndResponder = (result) => {
 | 
					  const onDragEnd: OnDragEndResponder = (result) => {
 | 
				
			||||||
    const { destination, source } = result;
 | 
					    const { destination, source } = result;
 | 
				
			||||||
@@ -126,8 +153,11 @@ export function ChatList(props: { narrow?: boolean }) {
 | 
				
			|||||||
                  navigate(Path.Chat);
 | 
					                  navigate(Path.Chat);
 | 
				
			||||||
                  selectSession(i);
 | 
					                  selectSession(i);
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
                onDelete={() => {
 | 
					                onDelete={async () => {
 | 
				
			||||||
                  if (!props.narrow || confirm(Locale.Home.DeleteChat)) {
 | 
					                  if (
 | 
				
			||||||
 | 
					                    (!props.narrow && !isMobileScreen) ||
 | 
				
			||||||
 | 
					                    (await showConfirm(Locale.Home.DeleteChat))
 | 
				
			||||||
 | 
					                  ) {
 | 
				
			||||||
                    chatStore.deleteSession(i);
 | 
					                    chatStore.deleteSession(i);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,58 @@
 | 
				
			|||||||
@import "../styles/animation.scss";
 | 
					@import "../styles/animation.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.attach-images {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 30px;
 | 
				
			||||||
 | 
					  bottom: 32px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.attach-image {
 | 
				
			||||||
 | 
					  cursor: default;
 | 
				
			||||||
 | 
					  width: 64px;
 | 
				
			||||||
 | 
					  height: 64px;
 | 
				
			||||||
 | 
					  border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
				
			||||||
 | 
					  border-radius: 5px;
 | 
				
			||||||
 | 
					  margin-right: 10px;
 | 
				
			||||||
 | 
					  background-size: cover;
 | 
				
			||||||
 | 
					  background-position: center;
 | 
				
			||||||
 | 
					  background-color: var(--white);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .attach-image-mask {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transition: all ease 0.2s;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .attach-image-mask:hover {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .delete-image {
 | 
				
			||||||
 | 
					    width: 24px;
 | 
				
			||||||
 | 
					    height: 24px;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    border-radius: 5px;
 | 
				
			||||||
 | 
					    float: right;
 | 
				
			||||||
 | 
					    background-color: var(--white);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-input-actions {
 | 
					.chat-input-actions {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-wrap: wrap;
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  gap: 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-end {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    margin-left: auto;
 | 
				
			||||||
 | 
					    gap: 5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chat-input-action {
 | 
					  .chat-input-action {
 | 
				
			||||||
    display: inline-flex;
 | 
					    display: inline-flex;
 | 
				
			||||||
@@ -14,12 +64,38 @@
 | 
				
			|||||||
    padding: 4px 10px;
 | 
					    padding: 4px 10px;
 | 
				
			||||||
    animation: slide-in ease 0.3s;
 | 
					    animation: slide-in ease 0.3s;
 | 
				
			||||||
    box-shadow: var(--card-shadow);
 | 
					    box-shadow: var(--card-shadow);
 | 
				
			||||||
    transition: all ease 0.3s;
 | 
					    transition: width ease 0.3s;
 | 
				
			||||||
    margin-bottom: 10px;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    height: 16px;
 | 
				
			||||||
 | 
					    width: var(--icon-width);
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &:not(:last-child) {
 | 
					    .text {
 | 
				
			||||||
      margin-right: 5px;
 | 
					      white-space: nowrap;
 | 
				
			||||||
 | 
					      padding-left: 5px;
 | 
				
			||||||
 | 
					      opacity: 0;
 | 
				
			||||||
 | 
					      transform: translateX(-5px);
 | 
				
			||||||
 | 
					      transition: all ease 0.3s;
 | 
				
			||||||
 | 
					      pointer-events: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      --delay: 0.5s;
 | 
				
			||||||
 | 
					      width: var(--full-width);
 | 
				
			||||||
 | 
					      transition-delay: var(--delay);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .text {
 | 
				
			||||||
 | 
					        transition-delay: var(--delay);
 | 
				
			||||||
 | 
					        opacity: 1;
 | 
				
			||||||
 | 
					        transform: translate(0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .text,
 | 
				
			||||||
 | 
					    .icon {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -68,11 +144,41 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.context-prompt {
 | 
					.context-prompt {
 | 
				
			||||||
 | 
					  .context-prompt-insert {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    padding: 4px;
 | 
				
			||||||
 | 
					    opacity: 0.2;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    background-color: rgba(0, 0, 0, 0);
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    margin-top: 4px;
 | 
				
			||||||
 | 
					    margin-bottom: 4px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      opacity: 1;
 | 
				
			||||||
 | 
					      background-color: rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .context-prompt-row {
 | 
					  .context-prompt-row {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    margin-bottom: 10px;
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      .context-drag {
 | 
				
			||||||
 | 
					        opacity: 1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .context-drag {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      opacity: 0.5;
 | 
				
			||||||
 | 
					      transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .context-role {
 | 
					    .context-role {
 | 
				
			||||||
      margin-right: 10px;
 | 
					      margin-right: 10px;
 | 
				
			||||||
@@ -107,3 +213,541 @@
 | 
				
			|||||||
    user-select: text;
 | 
					    user-select: text;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.clear-context {
 | 
				
			||||||
 | 
					  margin: 20px 0 0 0;
 | 
				
			||||||
 | 
					  padding: 4px 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  border-top: var(--border-in-light);
 | 
				
			||||||
 | 
					  border-bottom: var(--border-in-light);
 | 
				
			||||||
 | 
					  box-shadow: var(--card-shadow) inset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $linear: linear-gradient(
 | 
				
			||||||
 | 
					    to right,
 | 
				
			||||||
 | 
					    rgba(0, 0, 0, 0),
 | 
				
			||||||
 | 
					    rgba(0, 0, 0, 1),
 | 
				
			||||||
 | 
					    rgba(0, 0, 0, 0)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  mask-image: $linear;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @mixin show {
 | 
				
			||||||
 | 
					    transform: translateY(0);
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @mixin hide {
 | 
				
			||||||
 | 
					    transform: translateY(-50%);
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    transition: all ease 0.1s;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-tips {
 | 
				
			||||||
 | 
					    @include show;
 | 
				
			||||||
 | 
					    opacity: 0.5;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-revert-btn {
 | 
				
			||||||
 | 
					    color: var(--primary);
 | 
				
			||||||
 | 
					    @include hide;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					    border-color: var(--primary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .clear-context-tips {
 | 
				
			||||||
 | 
					      @include hide;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .clear-context-revert-btn {
 | 
				
			||||||
 | 
					      @include show;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-body {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  overflow: auto;
 | 
				
			||||||
 | 
					  overflow-x: hidden;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  padding-bottom: 40px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  overscroll-behavior: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-body-main-title {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    text-decoration: underline;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					  .chat-body-title {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: row;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:last-child {
 | 
				
			||||||
 | 
					    animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-user {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: row-reverse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-header {
 | 
				
			||||||
 | 
					    flex-direction: row-reverse;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-header {
 | 
				
			||||||
 | 
					  margin-top: 20px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-actions {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    align-items: flex-end;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    transform: scale(0.9) translateY(5px);
 | 
				
			||||||
 | 
					    margin: 0 10px;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .chat-input-actions {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-wrap: nowrap;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-model-name {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    color: var(--black);
 | 
				
			||||||
 | 
					    margin-left: 6px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-container {
 | 
				
			||||||
 | 
					  max-width: var(--message-max-width);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  align-items: flex-start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    .chat-message-edit {
 | 
				
			||||||
 | 
					      opacity: 0.9;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .chat-message-actions {
 | 
				
			||||||
 | 
					      opacity: 1;
 | 
				
			||||||
 | 
					      pointer-events: all;
 | 
				
			||||||
 | 
					      transform: scale(1) translateY(0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-user > .chat-message-container {
 | 
				
			||||||
 | 
					  align-items: flex-end;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-avatar {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-edit {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    button {
 | 
				
			||||||
 | 
					      padding: 7px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Specific styles for iOS devices */
 | 
				
			||||||
 | 
					  @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
 | 
				
			||||||
 | 
					    @supports (-webkit-touch-callout: none) {
 | 
				
			||||||
 | 
					      .chat-message-edit {
 | 
				
			||||||
 | 
					        top: -8%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-status {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #aaa;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  margin-top: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-tools {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #aaa;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  margin-top: 5px;
 | 
				
			||||||
 | 
					  .chat-message-tool {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: end;
 | 
				
			||||||
 | 
					    svg {
 | 
				
			||||||
 | 
					      margin-left: 5px;
 | 
				
			||||||
 | 
					      margin-right: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item {
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  max-width: 100%;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  background-color: rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  user-select: text;
 | 
				
			||||||
 | 
					  word-break: break-word;
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-audio {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  background-color: rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  user-select: text;
 | 
				
			||||||
 | 
					  word-break: break-word;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  audio {
 | 
				
			||||||
 | 
					    height: 30px; /* 调整高度 */
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item-image {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item-images {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  justify-content: left;
 | 
				
			||||||
 | 
					  grid-gap: 10px;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(var(--image-count), auto);
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item-image-multi {
 | 
				
			||||||
 | 
					  object-fit: cover;
 | 
				
			||||||
 | 
					  background-size: cover;
 | 
				
			||||||
 | 
					  background-position: center;
 | 
				
			||||||
 | 
					  background-repeat: no-repeat;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item-image,
 | 
				
			||||||
 | 
					.chat-message-item-image-multi {
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					  $calc-image-width: calc(100vw / 3 * 2 / var(--image-count));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-item-image-multi {
 | 
				
			||||||
 | 
					    width: $calc-image-width;
 | 
				
			||||||
 | 
					    height: $calc-image-width;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-item-image {
 | 
				
			||||||
 | 
					    max-width: calc(100vw / 3 * 2);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media screen and (min-width: 600px) {
 | 
				
			||||||
 | 
					  $max-image-width: calc(
 | 
				
			||||||
 | 
					    calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  $image-width: calc(
 | 
				
			||||||
 | 
					    calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 /
 | 
				
			||||||
 | 
					      var(--image-count)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-item-image-multi {
 | 
				
			||||||
 | 
					    width: $image-width;
 | 
				
			||||||
 | 
					    height: $image-width;
 | 
				
			||||||
 | 
					    max-width: $max-image-width;
 | 
				
			||||||
 | 
					    max-height: $max-image-width;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-item-image {
 | 
				
			||||||
 | 
					    max-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-action-date {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  opacity: 0.2;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  transition: all ease 0.6s;
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					  text-align: right;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  padding-right: 10px;
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-user > .chat-message-container > .chat-message-item {
 | 
				
			||||||
 | 
					  background-color: var(--second);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    min-width: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-panel {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  padding-top: 10px;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  border-top: var(--border-in-light);
 | 
				
			||||||
 | 
					  box-shadow: var(--card-shadow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-input-actions {
 | 
				
			||||||
 | 
					    .chat-input-action {
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin single-line {
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.prompt-hints {
 | 
				
			||||||
 | 
					  min-height: 20px;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  max-height: 50vh;
 | 
				
			||||||
 | 
					  overflow: auto;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column-reverse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  background-color: var(--white);
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					  box-shadow: var(--shadow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .prompt-hint {
 | 
				
			||||||
 | 
					    color: var(--black);
 | 
				
			||||||
 | 
					    padding: 6px 10px;
 | 
				
			||||||
 | 
					    animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    border: transparent 1px solid;
 | 
				
			||||||
 | 
					    margin: 4px;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:last-child) {
 | 
				
			||||||
 | 
					      margin-top: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .hint-title {
 | 
				
			||||||
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					      font-weight: bolder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @include single-line();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .hint-content {
 | 
				
			||||||
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @include single-line();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &-selected,
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      border-color: var(--primary);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-panel-inner {
 | 
				
			||||||
 | 
					  cursor: text;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-panel-inner-attach {
 | 
				
			||||||
 | 
					  padding-bottom: 80px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-panel-inner:has(.chat-input:focus) {
 | 
				
			||||||
 | 
					  border: 1px solid var(--primary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
 | 
				
			||||||
 | 
					  background-color: var(--white);
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					  font-family: inherit;
 | 
				
			||||||
 | 
					  padding: 10px 90px 10px 14px;
 | 
				
			||||||
 | 
					  resize: none;
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  min-height: 68px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input:focus {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-send {
 | 
				
			||||||
 | 
					  background-color: var(--primary);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  right: 30px;
 | 
				
			||||||
 | 
					  bottom: 32px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					  .chat-input {
 | 
				
			||||||
 | 
					    font-size: 16px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-input-send {
 | 
				
			||||||
 | 
					    bottom: 30px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-container {
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-grid {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
 | 
				
			||||||
 | 
					  gap: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-item {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  background-color: var(--white);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-title {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-keys {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 4px;
 | 
				
			||||||
 | 
					  background-color: var(--gray);
 | 
				
			||||||
 | 
					  min-width: 32px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key span {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-main {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  .chat-body-container {
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .chat-side-panel {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    inset: 0;
 | 
				
			||||||
 | 
					    background: var(--white);
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    z-index: 10;
 | 
				
			||||||
 | 
					    transform: translateX(100%);
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    &-show {
 | 
				
			||||||
 | 
					      transform: translateX(0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -6,11 +6,27 @@ import EmojiPicker, {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { ModelType } from "../store";
 | 
					import { ModelType } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					import BotIconDefault from "../icons/llm-icons/default.svg";
 | 
				
			||||||
import BlackBotIcon from "../icons/black-bot.svg";
 | 
					import BotIconOpenAI from "../icons/llm-icons/openai.svg";
 | 
				
			||||||
 | 
					import BotIconGemini from "../icons/llm-icons/gemini.svg";
 | 
				
			||||||
 | 
					import BotIconGemma from "../icons/llm-icons/gemma.svg";
 | 
				
			||||||
 | 
					import BotIconClaude from "../icons/llm-icons/claude.svg";
 | 
				
			||||||
 | 
					import BotIconMeta from "../icons/llm-icons/meta.svg";
 | 
				
			||||||
 | 
					import BotIconMistral from "../icons/llm-icons/mistral.svg";
 | 
				
			||||||
 | 
					import BotIconDeepseek from "../icons/llm-icons/deepseek.svg";
 | 
				
			||||||
 | 
					import BotIconMoonshot from "../icons/llm-icons/moonshot.svg";
 | 
				
			||||||
 | 
					import BotIconQwen from "../icons/llm-icons/qwen.svg";
 | 
				
			||||||
 | 
					import BotIconWenxin from "../icons/llm-icons/wenxin.svg";
 | 
				
			||||||
 | 
					import BotIconGrok from "../icons/llm-icons/grok.svg";
 | 
				
			||||||
 | 
					import BotIconHunyuan from "../icons/llm-icons/hunyuan.svg";
 | 
				
			||||||
 | 
					import BotIconDoubao from "../icons/llm-icons/doubao.svg";
 | 
				
			||||||
 | 
					import BotIconChatglm from "../icons/llm-icons/chatglm.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getEmojiUrl(unified: string, style: EmojiStyle) {
 | 
					export function getEmojiUrl(unified: string, style: EmojiStyle) {
 | 
				
			||||||
  return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
 | 
					  // Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
 | 
				
			||||||
 | 
					  // Old CDN broken, so I had to switch to this one
 | 
				
			||||||
 | 
					  // Author: https://github.com/H0llyW00dzZ
 | 
				
			||||||
 | 
					  return `https://fastly.jsdelivr.net/npm/emoji-datasource-apple/img/${style}/64/${unified}.png`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function AvatarPicker(props: {
 | 
					export function AvatarPicker(props: {
 | 
				
			||||||
@@ -18,6 +34,7 @@ export function AvatarPicker(props: {
 | 
				
			|||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <EmojiPicker
 | 
					    <EmojiPicker
 | 
				
			||||||
 | 
					      width={"100%"}
 | 
				
			||||||
      lazyLoadEmojis
 | 
					      lazyLoadEmojis
 | 
				
			||||||
      theme={EmojiTheme.AUTO}
 | 
					      theme={EmojiTheme.AUTO}
 | 
				
			||||||
      getEmojiUrl={getEmojiUrl}
 | 
					      getEmojiUrl={getEmojiUrl}
 | 
				
			||||||
@@ -29,14 +46,55 @@ export function AvatarPicker(props: {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Avatar(props: { model?: ModelType; avatar?: string }) {
 | 
					export function Avatar(props: { model?: ModelType; avatar?: string }) {
 | 
				
			||||||
 | 
					  let LlmIcon = BotIconDefault;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (props.model) {
 | 
					  if (props.model) {
 | 
				
			||||||
 | 
					    const modelName = props.model.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      modelName.startsWith("gpt") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("chatgpt") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("dall-e") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("dalle") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("o1") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("o3")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconOpenAI;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("gemini")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconGemini;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("gemma")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconGemma;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("claude")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconClaude;
 | 
				
			||||||
 | 
					    } else if (modelName.includes("llama")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconMeta;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("mixtral") || modelName.startsWith("codestral")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconMistral;
 | 
				
			||||||
 | 
					    } else if (modelName.includes("deepseek")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconDeepseek;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("moonshot")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconMoonshot;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("qwen")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconQwen;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("ernie")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconWenxin;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("grok")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconGrok;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("hunyuan")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconHunyuan;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("doubao") || modelName.startsWith("ep-")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconDoubao;
 | 
				
			||||||
 | 
					    } else if (
 | 
				
			||||||
 | 
					      modelName.includes("glm") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("cogview-") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("cogvideox-")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconChatglm;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className="no-dark">
 | 
					      <div className="no-dark">
 | 
				
			||||||
        {props.model?.startsWith("gpt-4") ? (
 | 
					        <LlmIcon className="user-avatar" width={30} height={30} />
 | 
				
			||||||
          <BlackBotIcon className="user-avatar" />
 | 
					 | 
				
			||||||
        ) : (
 | 
					 | 
				
			||||||
          <BotIcon className="user-avatar" />
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,14 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
import GithubIcon from "../icons/github.svg";
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
import ResetIcon from "../icons/reload.svg";
 | 
					import ResetIcon from "../icons/reload.svg";
 | 
				
			||||||
import { ISSUE_URL } from "../constant";
 | 
					import { ISSUE_URL } from "../constant";
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { downloadAs } from "../utils";
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
 | 
					import { useSyncStore } from "../store/sync";
 | 
				
			||||||
 | 
					import { useChatStore } from "../store/chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IErrorBoundaryState {
 | 
					interface IErrorBoundaryState {
 | 
				
			||||||
  hasError: boolean;
 | 
					  hasError: boolean;
 | 
				
			||||||
@@ -25,13 +29,9 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  clearAndSaveData() {
 | 
					  clearAndSaveData() {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      downloadAs(
 | 
					      useSyncStore.getState().export();
 | 
				
			||||||
        JSON.stringify(localStorage),
 | 
					 | 
				
			||||||
        "chatgpt-next-web-snapshot.json",
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      localStorage.clear();
 | 
					      useChatStore.getState().clearAllData();
 | 
				
			||||||
      location.reload();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,10 +57,11 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
 | 
				
			|||||||
            <IconButton
 | 
					            <IconButton
 | 
				
			||||||
              icon={<ResetIcon />}
 | 
					              icon={<ResetIcon />}
 | 
				
			||||||
              text="Clear All Data"
 | 
					              text="Clear All Data"
 | 
				
			||||||
              onClick={() =>
 | 
					              onClick={async () => {
 | 
				
			||||||
                confirm(Locale.Settings.Actions.ConfirmClearAll) &&
 | 
					                if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
 | 
				
			||||||
                this.clearAndSaveData()
 | 
					                  this.clearAndSaveData();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
              bordered
 | 
					              bordered
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										271
									
								
								app/components/exporter.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								app/components/exporter.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,271 @@
 | 
				
			|||||||
 | 
					.message-exporter {
 | 
				
			||||||
 | 
					  &-body {
 | 
				
			||||||
 | 
					    margin-top: 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.export-content {
 | 
				
			||||||
 | 
					  white-space: break-spaces;
 | 
				
			||||||
 | 
					  padding: 10px !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.steps {
 | 
				
			||||||
 | 
					  background-color: var(--gray);
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  padding: 5px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  box-shadow: var(--card-shadow) inset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .steps-progress {
 | 
				
			||||||
 | 
					    $padding: 5px;
 | 
				
			||||||
 | 
					    height: calc(100% - 2 * $padding);
 | 
				
			||||||
 | 
					    width: calc(100% - 2 * $padding);
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: $padding;
 | 
				
			||||||
 | 
					    left: $padding;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &-inner {
 | 
				
			||||||
 | 
					      box-sizing: border-box;
 | 
				
			||||||
 | 
					      box-shadow: var(--card-shadow);
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      content: "";
 | 
				
			||||||
 | 
					      display: inline-block;
 | 
				
			||||||
 | 
					      width: 0%;
 | 
				
			||||||
 | 
					      height: 100%;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					      transition: all ease 0.3s;
 | 
				
			||||||
 | 
					      border-radius: 8px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .steps-inner {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    transform: scale(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .step {
 | 
				
			||||||
 | 
					      flex-grow: 1;
 | 
				
			||||||
 | 
					      padding: 5px 10px;
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      color: var(--black);
 | 
				
			||||||
 | 
					      opacity: 0.5;
 | 
				
			||||||
 | 
					      transition: all ease 0.3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      $radius: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &-finished {
 | 
				
			||||||
 | 
					        opacity: 0.9;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover {
 | 
				
			||||||
 | 
					        opacity: 0.8;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &-current {
 | 
				
			||||||
 | 
					        color: var(--primary);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .step-index {
 | 
				
			||||||
 | 
					        background-color: var(--gray);
 | 
				
			||||||
 | 
					        border: var(--border-in-light);
 | 
				
			||||||
 | 
					        border-radius: 6px;
 | 
				
			||||||
 | 
					        display: inline-block;
 | 
				
			||||||
 | 
					        padding: 0px 5px;
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					        margin-right: 8px;
 | 
				
			||||||
 | 
					        opacity: 0.8;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .step-name {
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.preview-actions {
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  button {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:last-child) {
 | 
				
			||||||
 | 
					      margin-right: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.image-previewer {
 | 
				
			||||||
 | 
					  .preview-body {
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    box-shadow: var(--card-shadow) inset;
 | 
				
			||||||
 | 
					    background-color: var(--gray);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .chat-info {
 | 
				
			||||||
 | 
					      background-color: var(--second);
 | 
				
			||||||
 | 
					      padding: 20px;
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      margin-bottom: 20px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: space-between;
 | 
				
			||||||
 | 
					      align-items: flex-end;
 | 
				
			||||||
 | 
					      position: relative;
 | 
				
			||||||
 | 
					      overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					        flex-direction: column;
 | 
				
			||||||
 | 
					        align-items: flex-start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .icons {
 | 
				
			||||||
 | 
					          margin-bottom: 20px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .logo {
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        top: 0px;
 | 
				
			||||||
 | 
					        left: 0px;
 | 
				
			||||||
 | 
					        height: 50%;
 | 
				
			||||||
 | 
					        transform: scale(1.5);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .main-title {
 | 
				
			||||||
 | 
					        font-size: 20px;
 | 
				
			||||||
 | 
					        font-weight: bolder;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .sub-title {
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .icons {
 | 
				
			||||||
 | 
					        margin-top: 10px;
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .icon-space {
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          margin: 0 10px;
 | 
				
			||||||
 | 
					          font-weight: bolder;
 | 
				
			||||||
 | 
					          color: var(--primary);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .chat-info-item {
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					        color: var(--primary);
 | 
				
			||||||
 | 
					        padding: 2px 15px;
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        background-color: var(--white);
 | 
				
			||||||
 | 
					        box-shadow: var(--card-shadow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:not(:last-child) {
 | 
				
			||||||
 | 
					          margin-bottom: 5px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .message {
 | 
				
			||||||
 | 
					      margin-bottom: 20px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .avatar {
 | 
				
			||||||
 | 
					        margin-right: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .body {
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        padding: 8px 10px;
 | 
				
			||||||
 | 
					        max-width: calc(100% - 104px);
 | 
				
			||||||
 | 
					        box-shadow: var(--card-shadow);
 | 
				
			||||||
 | 
					        border: var(--border-in-light);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        code,
 | 
				
			||||||
 | 
					        pre {
 | 
				
			||||||
 | 
					          overflow: hidden;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .message-image {
 | 
				
			||||||
 | 
					          width: 100%;
 | 
				
			||||||
 | 
					          margin-top: 10px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .message-images {
 | 
				
			||||||
 | 
					          display: grid;
 | 
				
			||||||
 | 
					          justify-content: left;
 | 
				
			||||||
 | 
					          grid-gap: 10px;
 | 
				
			||||||
 | 
					          grid-template-columns: repeat(var(--image-count), auto);
 | 
				
			||||||
 | 
					          margin-top: 10px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					          $image-width: calc(calc(100vw/2)/var(--image-count));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          .message-image-multi {
 | 
				
			||||||
 | 
					            width: $image-width;
 | 
				
			||||||
 | 
					            height: $image-width;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          .message-image {
 | 
				
			||||||
 | 
					            max-width: calc(100vw/3*2);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @media screen and (min-width: 600px) {
 | 
				
			||||||
 | 
					          $max-image-width: calc(900px/3*2/var(--image-count));
 | 
				
			||||||
 | 
					          $image-width: calc(80vw/3*2/var(--image-count));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          .message-image-multi {
 | 
				
			||||||
 | 
					            width: $image-width;
 | 
				
			||||||
 | 
					            height: $image-width;
 | 
				
			||||||
 | 
					            max-width: $max-image-width;
 | 
				
			||||||
 | 
					            max-height: $max-image-width;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          .message-image {
 | 
				
			||||||
 | 
					            max-width: calc(100vw/3*2);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .message-image-multi {
 | 
				
			||||||
 | 
					          object-fit: cover;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .message-image,
 | 
				
			||||||
 | 
					        .message-image-multi {
 | 
				
			||||||
 | 
					          box-sizing: border-box;
 | 
				
			||||||
 | 
					          border-radius: 10px;
 | 
				
			||||||
 | 
					          border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &-assistant {
 | 
				
			||||||
 | 
					        .body {
 | 
				
			||||||
 | 
					          background-color: var(--white);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &-user {
 | 
				
			||||||
 | 
					        flex-direction: row-reverse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .avatar {
 | 
				
			||||||
 | 
					          margin-right: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .body {
 | 
				
			||||||
 | 
					          background-color: var(--second);
 | 
				
			||||||
 | 
					          margin-right: 10px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .default-theme {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										694
									
								
								app/components/exporter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										694
									
								
								app/components/exporter.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,694 @@
 | 
				
			|||||||
 | 
					/* eslint-disable @next/next/no-img-element */
 | 
				
			||||||
 | 
					import { ChatMessage, useAppConfig, useChatStore } from "../store";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					import styles from "./exporter.module.scss";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  List,
 | 
				
			||||||
 | 
					  ListItem,
 | 
				
			||||||
 | 
					  Modal,
 | 
				
			||||||
 | 
					  Select,
 | 
				
			||||||
 | 
					  showImageModal,
 | 
				
			||||||
 | 
					  showModal,
 | 
				
			||||||
 | 
					  showToast,
 | 
				
			||||||
 | 
					} from "./ui-lib";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  copyToClipboard,
 | 
				
			||||||
 | 
					  downloadAs,
 | 
				
			||||||
 | 
					  getMessageImages,
 | 
				
			||||||
 | 
					  useMobileScreen,
 | 
				
			||||||
 | 
					} from "../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import CopyIcon from "../icons/copy.svg";
 | 
				
			||||||
 | 
					import LoadingIcon from "../icons/three-dots.svg";
 | 
				
			||||||
 | 
					import ChatGptIcon from "../icons/chatgpt.png";
 | 
				
			||||||
 | 
					import ShareIcon from "../icons/share.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import DownloadIcon from "../icons/download.svg";
 | 
				
			||||||
 | 
					import { useEffect, useMemo, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { MessageSelector, useMessageSelector } from "./message-selector";
 | 
				
			||||||
 | 
					import { Avatar } from "./emoji";
 | 
				
			||||||
 | 
					import dynamic from "next/dynamic";
 | 
				
			||||||
 | 
					import NextImage from "next/image";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { toBlob, toPng } from "html-to-image";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { prettyObject } from "../utils/format";
 | 
				
			||||||
 | 
					import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
 | 
				
			||||||
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
 | 
					import { type ClientApi, getClientApi } from "../client/api";
 | 
				
			||||||
 | 
					import { getMessageTextContent } from "../utils";
 | 
				
			||||||
 | 
					import { MaskAvatar } from "./mask";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
 | 
				
			||||||
 | 
					  loading: () => <LoadingIcon />,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ExportMessageModal(props: { onClose: () => void }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="modal-mask">
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        title={Locale.Export.Title}
 | 
				
			||||||
 | 
					        onClose={props.onClose}
 | 
				
			||||||
 | 
					        footer={
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              width: "100%",
 | 
				
			||||||
 | 
					              textAlign: "center",
 | 
				
			||||||
 | 
					              fontSize: 14,
 | 
				
			||||||
 | 
					              opacity: 0.5,
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {Locale.Exporter.Description.Title}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div style={{ minHeight: "40vh" }}>
 | 
				
			||||||
 | 
					          <MessageExporter />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function useSteps(
 | 
				
			||||||
 | 
					  steps: Array<{
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    value: string;
 | 
				
			||||||
 | 
					  }>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const stepCount = steps.length;
 | 
				
			||||||
 | 
					  const [currentStepIndex, setCurrentStepIndex] = useState(0);
 | 
				
			||||||
 | 
					  const nextStep = () =>
 | 
				
			||||||
 | 
					    setCurrentStepIndex((currentStepIndex + 1) % stepCount);
 | 
				
			||||||
 | 
					  const prevStep = () =>
 | 
				
			||||||
 | 
					    setCurrentStepIndex((currentStepIndex - 1 + stepCount) % stepCount);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    currentStepIndex,
 | 
				
			||||||
 | 
					    setCurrentStepIndex,
 | 
				
			||||||
 | 
					    nextStep,
 | 
				
			||||||
 | 
					    prevStep,
 | 
				
			||||||
 | 
					    currentStep: steps[currentStepIndex],
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Steps<
 | 
				
			||||||
 | 
					  T extends {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    value: string;
 | 
				
			||||||
 | 
					  }[],
 | 
				
			||||||
 | 
					>(props: { steps: T; onStepChange?: (index: number) => void; index: number }) {
 | 
				
			||||||
 | 
					  const steps = props.steps;
 | 
				
			||||||
 | 
					  const stepCount = steps.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["steps"]}>
 | 
				
			||||||
 | 
					      <div className={styles["steps-progress"]}>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={styles["steps-progress-inner"]}
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            width: `${((props.index + 1) / stepCount) * 100}%`,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        ></div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className={styles["steps-inner"]}>
 | 
				
			||||||
 | 
					        {steps.map((step, i) => {
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              key={i}
 | 
				
			||||||
 | 
					              className={clsx("clickable", styles["step"], {
 | 
				
			||||||
 | 
					                [styles["step-finished"]]: i <= props.index,
 | 
				
			||||||
 | 
					                [styles["step-current"]]: i === props.index,
 | 
				
			||||||
 | 
					              })}
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                props.onStepChange?.(i);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              role="button"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <span className={styles["step-index"]}>{i + 1}</span>
 | 
				
			||||||
 | 
					              <span className={styles["step-name"]}>{step.name}</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function MessageExporter() {
 | 
				
			||||||
 | 
					  const steps = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      name: Locale.Export.Steps.Select,
 | 
				
			||||||
 | 
					      value: "select",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      name: Locale.Export.Steps.Preview,
 | 
				
			||||||
 | 
					      value: "preview",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					  const { currentStep, setCurrentStepIndex, currentStepIndex } =
 | 
				
			||||||
 | 
					    useSteps(steps);
 | 
				
			||||||
 | 
					  const formats = ["text", "image", "json"] as const;
 | 
				
			||||||
 | 
					  type ExportFormat = (typeof formats)[number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [exportConfig, setExportConfig] = useState({
 | 
				
			||||||
 | 
					    format: "image" as ExportFormat,
 | 
				
			||||||
 | 
					    includeContext: true,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function updateExportConfig(updater: (config: typeof exportConfig) => void) {
 | 
				
			||||||
 | 
					    const config = { ...exportConfig };
 | 
				
			||||||
 | 
					    updater(config);
 | 
				
			||||||
 | 
					    setExportConfig(config);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const { selection, updateSelection } = useMessageSelector();
 | 
				
			||||||
 | 
					  const selectedMessages = useMemo(() => {
 | 
				
			||||||
 | 
					    const ret: ChatMessage[] = [];
 | 
				
			||||||
 | 
					    if (exportConfig.includeContext) {
 | 
				
			||||||
 | 
					      ret.push(...session.mask.context);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ret.push(...session.messages.filter((m) => selection.has(m.id)));
 | 
				
			||||||
 | 
					    return ret;
 | 
				
			||||||
 | 
					  }, [
 | 
				
			||||||
 | 
					    exportConfig.includeContext,
 | 
				
			||||||
 | 
					    session.messages,
 | 
				
			||||||
 | 
					    session.mask.context,
 | 
				
			||||||
 | 
					    selection,
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					  function preview() {
 | 
				
			||||||
 | 
					    if (exportConfig.format === "text") {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <MarkdownPreviewer messages={selectedMessages} topic={session.topic} />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else if (exportConfig.format === "json") {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <JsonPreviewer messages={selectedMessages} topic={session.topic} />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <ImagePreviewer messages={selectedMessages} topic={session.topic} />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <Steps
 | 
				
			||||||
 | 
					        steps={steps}
 | 
				
			||||||
 | 
					        index={currentStepIndex}
 | 
				
			||||||
 | 
					        onStepChange={setCurrentStepIndex}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={styles["message-exporter-body"]}
 | 
				
			||||||
 | 
					        style={currentStep.value !== "select" ? { display: "none" } : {}}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <List>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Export.Format.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Export.Format.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Select
 | 
				
			||||||
 | 
					              value={exportConfig.format}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                updateExportConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.format = e.currentTarget.value as ExportFormat),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {formats.map((f) => (
 | 
				
			||||||
 | 
					                <option key={f} value={f}>
 | 
				
			||||||
 | 
					                  {f}
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </Select>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Export.IncludeContext.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Export.IncludeContext.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={exportConfig.includeContext}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                updateExportConfig(
 | 
				
			||||||
 | 
					                  (config) => (config.includeContext = e.currentTarget.checked),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					        <MessageSelector
 | 
				
			||||||
 | 
					          selection={selection}
 | 
				
			||||||
 | 
					          updateSelection={updateSelection}
 | 
				
			||||||
 | 
					          defaultSelectAll
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {currentStep.value === "preview" && (
 | 
				
			||||||
 | 
					        <div className={styles["message-exporter-body"]}>{preview()}</div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function RenderExport(props: {
 | 
				
			||||||
 | 
					  messages: ChatMessage[];
 | 
				
			||||||
 | 
					  onRender: (messages: ChatMessage[]) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const domRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!domRef.current) return;
 | 
				
			||||||
 | 
					    const dom = domRef.current;
 | 
				
			||||||
 | 
					    const messages = Array.from(
 | 
				
			||||||
 | 
					      dom.getElementsByClassName(EXPORT_MESSAGE_CLASS_NAME),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (messages.length !== props.messages.length) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const renderMsgs = messages.map((v, i) => {
 | 
				
			||||||
 | 
					      const [role, _] = v.id.split(":");
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        id: i.toString(),
 | 
				
			||||||
 | 
					        role: role as any,
 | 
				
			||||||
 | 
					        content: role === "user" ? v.textContent ?? "" : v.innerHTML,
 | 
				
			||||||
 | 
					        date: "",
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    props.onRender(renderMsgs);
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div ref={domRef}>
 | 
				
			||||||
 | 
					      {props.messages.map((m, i) => (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          key={i}
 | 
				
			||||||
 | 
					          id={`${m.role}:${i}`}
 | 
				
			||||||
 | 
					          className={EXPORT_MESSAGE_CLASS_NAME}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Markdown content={getMessageTextContent(m)} defaultShow />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ))}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function PreviewActions(props: {
 | 
				
			||||||
 | 
					  download: () => void;
 | 
				
			||||||
 | 
					  copy: () => void;
 | 
				
			||||||
 | 
					  showCopy?: boolean;
 | 
				
			||||||
 | 
					  messages?: ChatMessage[];
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					  const [shouldExport, setShouldExport] = useState(false);
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const onRenderMsgs = (msgs: ChatMessage[]) => {
 | 
				
			||||||
 | 
					    setShouldExport(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const api: ClientApi = getClientApi(config.modelConfig.providerName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    api
 | 
				
			||||||
 | 
					      .share(msgs)
 | 
				
			||||||
 | 
					      .then((res) => {
 | 
				
			||||||
 | 
					        if (!res) return;
 | 
				
			||||||
 | 
					        showModal({
 | 
				
			||||||
 | 
					          title: Locale.Export.Share,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              value={res}
 | 
				
			||||||
 | 
					              key="input"
 | 
				
			||||||
 | 
					              style={{
 | 
				
			||||||
 | 
					                width: "100%",
 | 
				
			||||||
 | 
					                maxWidth: "unset",
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              readOnly
 | 
				
			||||||
 | 
					              onClick={(e) => e.currentTarget.select()}
 | 
				
			||||||
 | 
					            ></input>,
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          actions: [
 | 
				
			||||||
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              icon={<CopyIcon />}
 | 
				
			||||||
 | 
					              text={Locale.Chat.Actions.Copy}
 | 
				
			||||||
 | 
					              key="copy"
 | 
				
			||||||
 | 
					              onClick={() => copyToClipboard(res)}
 | 
				
			||||||
 | 
					            />,
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					          window.open(res, "_blank");
 | 
				
			||||||
 | 
					        }, 800);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch((e) => {
 | 
				
			||||||
 | 
					        console.error("[Share]", e);
 | 
				
			||||||
 | 
					        showToast(prettyObject(e));
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .finally(() => setLoading(false));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const share = async () => {
 | 
				
			||||||
 | 
					    if (props.messages?.length) {
 | 
				
			||||||
 | 
					      setLoading(true);
 | 
				
			||||||
 | 
					      setShouldExport(true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div className={styles["preview-actions"]}>
 | 
				
			||||||
 | 
					        {props.showCopy && (
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            text={Locale.Export.Copy}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            shadow
 | 
				
			||||||
 | 
					            icon={<CopyIcon />}
 | 
				
			||||||
 | 
					            onClick={props.copy}
 | 
				
			||||||
 | 
					          ></IconButton>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          text={Locale.Export.Download}
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					          shadow
 | 
				
			||||||
 | 
					          icon={<DownloadIcon />}
 | 
				
			||||||
 | 
					          onClick={props.download}
 | 
				
			||||||
 | 
					        ></IconButton>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          text={Locale.Export.Share}
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					          shadow
 | 
				
			||||||
 | 
					          icon={loading ? <LoadingIcon /> : <ShareIcon />}
 | 
				
			||||||
 | 
					          onClick={share}
 | 
				
			||||||
 | 
					        ></IconButton>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        style={{
 | 
				
			||||||
 | 
					          position: "fixed",
 | 
				
			||||||
 | 
					          right: "200vw",
 | 
				
			||||||
 | 
					          pointerEvents: "none",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {shouldExport && (
 | 
				
			||||||
 | 
					          <RenderExport
 | 
				
			||||||
 | 
					            messages={props.messages ?? []}
 | 
				
			||||||
 | 
					            onRender={onRenderMsgs}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ImagePreviewer(props: {
 | 
				
			||||||
 | 
					  messages: ChatMessage[];
 | 
				
			||||||
 | 
					  topic: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const mask = session.mask;
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const previewRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const copy = () => {
 | 
				
			||||||
 | 
					    showToast(Locale.Export.Image.Toast);
 | 
				
			||||||
 | 
					    const dom = previewRef.current;
 | 
				
			||||||
 | 
					    if (!dom) return;
 | 
				
			||||||
 | 
					    toBlob(dom).then((blob) => {
 | 
				
			||||||
 | 
					      if (!blob) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        navigator.clipboard
 | 
				
			||||||
 | 
					          .write([
 | 
				
			||||||
 | 
					            new ClipboardItem({
 | 
				
			||||||
 | 
					              "image/png": blob,
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					          ])
 | 
				
			||||||
 | 
					          .then(() => {
 | 
				
			||||||
 | 
					            showToast(Locale.Copy.Success);
 | 
				
			||||||
 | 
					            refreshPreview();
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error("[Copy Image] ", e);
 | 
				
			||||||
 | 
					        showToast(Locale.Copy.Failed);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isMobile = useMobileScreen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const download = async () => {
 | 
				
			||||||
 | 
					    showToast(Locale.Export.Image.Toast);
 | 
				
			||||||
 | 
					    const dom = previewRef.current;
 | 
				
			||||||
 | 
					    if (!dom) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isApp = getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const blob = await toPng(dom);
 | 
				
			||||||
 | 
					      if (!blob) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isMobile || (isApp && window.__TAURI__)) {
 | 
				
			||||||
 | 
					        if (isApp && window.__TAURI__) {
 | 
				
			||||||
 | 
					          const result = await window.__TAURI__.dialog.save({
 | 
				
			||||||
 | 
					            defaultPath: `${props.topic}.png`,
 | 
				
			||||||
 | 
					            filters: [
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                name: "PNG Files",
 | 
				
			||||||
 | 
					                extensions: ["png"],
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                name: "All Files",
 | 
				
			||||||
 | 
					                extensions: ["*"],
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (result !== null) {
 | 
				
			||||||
 | 
					            const response = await fetch(blob);
 | 
				
			||||||
 | 
					            const buffer = await response.arrayBuffer();
 | 
				
			||||||
 | 
					            const uint8Array = new Uint8Array(buffer);
 | 
				
			||||||
 | 
					            await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
 | 
				
			||||||
 | 
					            showToast(Locale.Download.Success);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            showToast(Locale.Download.Failed);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          showImageModal(blob);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const link = document.createElement("a");
 | 
				
			||||||
 | 
					        link.download = `${props.topic}.png`;
 | 
				
			||||||
 | 
					        link.href = blob;
 | 
				
			||||||
 | 
					        link.click();
 | 
				
			||||||
 | 
					        refreshPreview();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast(Locale.Download.Failed);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const refreshPreview = () => {
 | 
				
			||||||
 | 
					    const dom = previewRef.current;
 | 
				
			||||||
 | 
					    if (dom) {
 | 
				
			||||||
 | 
					      dom.innerHTML = dom.innerHTML; // Refresh the content of the preview by resetting its HTML for fix a bug glitching
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["image-previewer"]}>
 | 
				
			||||||
 | 
					      <PreviewActions
 | 
				
			||||||
 | 
					        copy={copy}
 | 
				
			||||||
 | 
					        download={download}
 | 
				
			||||||
 | 
					        showCopy={!isMobile}
 | 
				
			||||||
 | 
					        messages={props.messages}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={clsx(styles["preview-body"], styles["default-theme"])}
 | 
				
			||||||
 | 
					        ref={previewRef}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div className={styles["chat-info"]}>
 | 
				
			||||||
 | 
					          <div className={clsx(styles["logo"], "no-dark")}>
 | 
				
			||||||
 | 
					            <NextImage
 | 
				
			||||||
 | 
					              src={ChatGptIcon.src}
 | 
				
			||||||
 | 
					              alt="logo"
 | 
				
			||||||
 | 
					              width={50}
 | 
				
			||||||
 | 
					              height={50}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <div className={styles["main-title"]}>NextChat</div>
 | 
				
			||||||
 | 
					            <div className={styles["sub-title"]}>
 | 
				
			||||||
 | 
					              github.com/ChatGPTNextWeb/ChatGPT-Next-Web
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className={styles["icons"]}>
 | 
				
			||||||
 | 
					              <MaskAvatar avatar={config.avatar} />
 | 
				
			||||||
 | 
					              <span className={styles["icon-space"]}>&</span>
 | 
				
			||||||
 | 
					              <MaskAvatar
 | 
				
			||||||
 | 
					                avatar={mask.avatar}
 | 
				
			||||||
 | 
					                model={session.mask.modelConfig.model}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <div className={styles["chat-info-item"]}>
 | 
				
			||||||
 | 
					              {Locale.Exporter.Model}: {mask.modelConfig.model}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className={styles["chat-info-item"]}>
 | 
				
			||||||
 | 
					              {Locale.Exporter.Messages}: {props.messages.length}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className={styles["chat-info-item"]}>
 | 
				
			||||||
 | 
					              {Locale.Exporter.Topic}: {session.topic}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className={styles["chat-info-item"]}>
 | 
				
			||||||
 | 
					              {Locale.Exporter.Time}:{" "}
 | 
				
			||||||
 | 
					              {new Date(
 | 
				
			||||||
 | 
					                props.messages.at(-1)?.date ?? Date.now(),
 | 
				
			||||||
 | 
					              ).toLocaleString()}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {props.messages.map((m, i) => {
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              className={clsx(styles["message"], styles["message-" + m.role])}
 | 
				
			||||||
 | 
					              key={i}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className={styles["avatar"]}>
 | 
				
			||||||
 | 
					                {m.role === "user" ? (
 | 
				
			||||||
 | 
					                  <Avatar avatar={config.avatar}></Avatar>
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <MaskAvatar
 | 
				
			||||||
 | 
					                    avatar={session.mask.avatar}
 | 
				
			||||||
 | 
					                    model={m.model || session.mask.modelConfig.model}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div className={styles["body"]}>
 | 
				
			||||||
 | 
					                <Markdown
 | 
				
			||||||
 | 
					                  content={getMessageTextContent(m)}
 | 
				
			||||||
 | 
					                  fontSize={config.fontSize}
 | 
				
			||||||
 | 
					                  fontFamily={config.fontFamily}
 | 
				
			||||||
 | 
					                  defaultShow
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                {getMessageImages(m).length == 1 && (
 | 
				
			||||||
 | 
					                  <img
 | 
				
			||||||
 | 
					                    key={i}
 | 
				
			||||||
 | 
					                    src={getMessageImages(m)[0]}
 | 
				
			||||||
 | 
					                    alt="message"
 | 
				
			||||||
 | 
					                    className={styles["message-image"]}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                {getMessageImages(m).length > 1 && (
 | 
				
			||||||
 | 
					                  <div
 | 
				
			||||||
 | 
					                    className={styles["message-images"]}
 | 
				
			||||||
 | 
					                    style={
 | 
				
			||||||
 | 
					                      {
 | 
				
			||||||
 | 
					                        "--image-count": getMessageImages(m).length,
 | 
				
			||||||
 | 
					                      } as React.CSSProperties
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {getMessageImages(m).map((src, i) => (
 | 
				
			||||||
 | 
					                      <img
 | 
				
			||||||
 | 
					                        key={i}
 | 
				
			||||||
 | 
					                        src={src}
 | 
				
			||||||
 | 
					                        alt="message"
 | 
				
			||||||
 | 
					                        className={styles["message-image-multi"]}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    ))}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function MarkdownPreviewer(props: {
 | 
				
			||||||
 | 
					  messages: ChatMessage[];
 | 
				
			||||||
 | 
					  topic: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const mdText =
 | 
				
			||||||
 | 
					    `# ${props.topic}\n\n` +
 | 
				
			||||||
 | 
					    props.messages
 | 
				
			||||||
 | 
					      .map((m) => {
 | 
				
			||||||
 | 
					        return m.role === "user"
 | 
				
			||||||
 | 
					          ? `## ${Locale.Export.MessageFromYou}:\n${getMessageTextContent(m)}`
 | 
				
			||||||
 | 
					          : `## ${Locale.Export.MessageFromChatGPT}:\n${getMessageTextContent(
 | 
				
			||||||
 | 
					              m,
 | 
				
			||||||
 | 
					            ).trim()}`;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .join("\n\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const copy = () => {
 | 
				
			||||||
 | 
					    copyToClipboard(mdText);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const download = () => {
 | 
				
			||||||
 | 
					    downloadAs(mdText, `${props.topic}.md`);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <PreviewActions
 | 
				
			||||||
 | 
					        copy={copy}
 | 
				
			||||||
 | 
					        download={download}
 | 
				
			||||||
 | 
					        showCopy={true}
 | 
				
			||||||
 | 
					        messages={props.messages}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <div className="markdown-body">
 | 
				
			||||||
 | 
					        <pre className={styles["export-content"]}>{mdText}</pre>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function JsonPreviewer(props: {
 | 
				
			||||||
 | 
					  messages: ChatMessage[];
 | 
				
			||||||
 | 
					  topic: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const msgs = {
 | 
				
			||||||
 | 
					    messages: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        role: "system",
 | 
				
			||||||
 | 
					        content: `${Locale.FineTuned.Sysmessage} ${props.topic}`,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      ...props.messages.map((m) => ({
 | 
				
			||||||
 | 
					        role: m.role,
 | 
				
			||||||
 | 
					        content: m.content,
 | 
				
			||||||
 | 
					      })),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const mdText = "```json\n" + JSON.stringify(msgs, null, 2) + "\n```";
 | 
				
			||||||
 | 
					  const minifiedJson = JSON.stringify(msgs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const copy = () => {
 | 
				
			||||||
 | 
					    copyToClipboard(minifiedJson);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const download = () => {
 | 
				
			||||||
 | 
					    downloadAs(JSON.stringify(msgs), `${props.topic}.json`);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <PreviewActions
 | 
				
			||||||
 | 
					        copy={copy}
 | 
				
			||||||
 | 
					        download={download}
 | 
				
			||||||
 | 
					        showCopy={false}
 | 
				
			||||||
 | 
					        messages={props.messages}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <div className="markdown-body" onClick={copy}>
 | 
				
			||||||
 | 
					        <Markdown content={mdText} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,7 +6,7 @@
 | 
				
			|||||||
  color: var(--black);
 | 
					  color: var(--black);
 | 
				
			||||||
  background-color: var(--white);
 | 
					  background-color: var(--white);
 | 
				
			||||||
  min-width: 600px;
 | 
					  min-width: 600px;
 | 
				
			||||||
  min-height: 480px;
 | 
					  min-height: 370px;
 | 
				
			||||||
  max-width: 1200px;
 | 
					  max-width: 1200px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -61,24 +61,36 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover,
 | 
				
			||||||
 | 
					  &:active {
 | 
				
			||||||
 | 
					    .sidebar-drag {
 | 
				
			||||||
 | 
					      background-color: rgba($color: #000000, $alpha: 0.01);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      svg {
 | 
				
			||||||
 | 
					        opacity: 0.2;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-drag {
 | 
					.sidebar-drag {
 | 
				
			||||||
  $width: 10px;
 | 
					  $width: 14px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: 0;
 | 
					  top: 0;
 | 
				
			||||||
  right: 0;
 | 
					  right: 0;
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  width: $width;
 | 
					  width: $width;
 | 
				
			||||||
  background-color: var(--black);
 | 
					  background-color: rgba($color: #000000, $alpha: 0);
 | 
				
			||||||
  cursor: ew-resize;
 | 
					  cursor: ew-resize;
 | 
				
			||||||
  opacity: 0;
 | 
					 | 
				
			||||||
  transition: all ease 0.3s;
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover,
 | 
					  svg {
 | 
				
			||||||
  &:active {
 | 
					    opacity: 0;
 | 
				
			||||||
    opacity: 0.2;
 | 
					    margin-left: -2px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -125,12 +137,21 @@
 | 
				
			|||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  padding-top: 20px;
 | 
					  padding-top: 20px;
 | 
				
			||||||
  padding-bottom: 20px;
 | 
					  padding-bottom: 20px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  &-narrow {
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-logo {
 | 
					.sidebar-logo {
 | 
				
			||||||
  position: absolute;
 | 
					  display: inline-flex;
 | 
				
			||||||
  right: 0;
 | 
					}
 | 
				
			||||||
  bottom: 18px;
 | 
					
 | 
				
			||||||
 | 
					.sidebar-title-container {
 | 
				
			||||||
 | 
					  display: inline-flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-title {
 | 
					.sidebar-title {
 | 
				
			||||||
@@ -141,7 +162,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.sidebar-sub-title {
 | 
					.sidebar-sub-title {
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  font-weight: 400px;
 | 
					  font-weight: 400;
 | 
				
			||||||
  animation: slide-in ease 0.3s;
 | 
					  animation: slide-in ease 0.3s;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -162,6 +183,7 @@
 | 
				
			|||||||
  user-select: none;
 | 
					  user-select: none;
 | 
				
			||||||
  border: 2px solid transparent;
 | 
					  border: 2px solid transparent;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  content-visibility: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-item:hover {
 | 
					.chat-item:hover {
 | 
				
			||||||
@@ -176,7 +198,7 @@
 | 
				
			|||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  font-weight: bolder;
 | 
					  font-weight: bolder;
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  width: 200px;
 | 
					  width: calc(100% - 15px);
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
  text-overflow: ellipsis;
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
  white-space: nowrap;
 | 
					  white-space: nowrap;
 | 
				
			||||||
@@ -185,8 +207,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.chat-item-delete {
 | 
					.chat-item-delete {
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: 10px;
 | 
					  top: 0;
 | 
				
			||||||
  right: -20px;
 | 
					  right: 0;
 | 
				
			||||||
  transition: all ease 0.3s;
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
  opacity: 0;
 | 
					  opacity: 0;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
@@ -194,7 +216,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.chat-item:hover > .chat-item-delete {
 | 
					.chat-item:hover > .chat-item-delete {
 | 
				
			||||||
  opacity: 0.5;
 | 
					  opacity: 0.5;
 | 
				
			||||||
  right: 10px;
 | 
					  transform: translateX(-4px);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-item:hover > .chat-item-delete:hover {
 | 
					.chat-item:hover > .chat-item-delete:hover {
 | 
				
			||||||
@@ -283,15 +305,6 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chat-item-delete {
 | 
					 | 
				
			||||||
    top: 15px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .chat-item:hover > .chat-item-delete {
 | 
					 | 
				
			||||||
    opacity: 0.5;
 | 
					 | 
				
			||||||
    right: 5px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .sidebar-tail {
 | 
					  .sidebar-tail {
 | 
				
			||||||
    flex-direction: column-reverse;
 | 
					    flex-direction: column-reverse;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
@@ -322,246 +335,6 @@
 | 
				
			|||||||
  margin-right: 15px;
 | 
					  margin-right: 15px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-body {
 | 
					 | 
				
			||||||
  flex: 1;
 | 
					 | 
				
			||||||
  overflow: auto;
 | 
					 | 
				
			||||||
  padding: 20px;
 | 
					 | 
				
			||||||
  padding-bottom: 40px;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-body-title {
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:hover {
 | 
					 | 
				
			||||||
    text-decoration: underline;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: row;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:last-child {
 | 
					 | 
				
			||||||
    animation: slide-in ease 0.3s;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-user {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: row-reverse;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-container {
 | 
					 | 
				
			||||||
  max-width: var(--message-max-width);
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  align-items: flex-start;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:hover {
 | 
					 | 
				
			||||||
    .chat-message-top-actions {
 | 
					 | 
				
			||||||
      opacity: 1;
 | 
					 | 
				
			||||||
      right: 10px;
 | 
					 | 
				
			||||||
      pointer-events: all;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-user > .chat-message-container {
 | 
					 | 
				
			||||||
  align-items: flex-end;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-avatar {
 | 
					 | 
				
			||||||
  margin-top: 20px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-status {
 | 
					 | 
				
			||||||
  font-size: 12px;
 | 
					 | 
				
			||||||
  color: #aaa;
 | 
					 | 
				
			||||||
  line-height: 1.5;
 | 
					 | 
				
			||||||
  margin-top: 5px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-item {
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  max-width: 100%;
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
  border-radius: 10px;
 | 
					 | 
				
			||||||
  background-color: rgba(0, 0, 0, 0.05);
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  user-select: text;
 | 
					 | 
				
			||||||
  word-break: break-word;
 | 
					 | 
				
			||||||
  border: var(--border-in-light);
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-top-actions {
 | 
					 | 
				
			||||||
  font-size: 12px;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  right: 20px;
 | 
					 | 
				
			||||||
  top: -26px;
 | 
					 | 
				
			||||||
  left: 100px;
 | 
					 | 
				
			||||||
  transition: all ease 0.3s;
 | 
					 | 
				
			||||||
  opacity: 0;
 | 
					 | 
				
			||||||
  pointer-events: none;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: row-reverse;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .chat-message-top-action {
 | 
					 | 
				
			||||||
    opacity: 0.5;
 | 
					 | 
				
			||||||
    color: var(--black);
 | 
					 | 
				
			||||||
    white-space: nowrap;
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:hover {
 | 
					 | 
				
			||||||
      opacity: 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:not(:first-child) {
 | 
					 | 
				
			||||||
      margin-right: 10px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-user > .chat-message-container > .chat-message-item {
 | 
					 | 
				
			||||||
  background-color: var(--second);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-actions {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: row-reverse;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  padding-top: 5px;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  font-size: 12px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-message-action-date {
 | 
					 | 
				
			||||||
  color: #aaa;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-input-panel {
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  padding: 20px;
 | 
					 | 
				
			||||||
  padding-top: 10px;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  border-top-left-radius: 10px;
 | 
					 | 
				
			||||||
  border-top-right-radius: 10px;
 | 
					 | 
				
			||||||
  border-top: var(--border-in-light);
 | 
					 | 
				
			||||||
  box-shadow: var(--card-shadow);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@mixin single-line {
 | 
					 | 
				
			||||||
  white-space: nowrap;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  text-overflow: ellipsis;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.prompt-hints {
 | 
					 | 
				
			||||||
  min-height: 20px;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  max-height: 50vh;
 | 
					 | 
				
			||||||
  overflow: auto;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column-reverse;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  background-color: var(--white);
 | 
					 | 
				
			||||||
  border: var(--border-in-light);
 | 
					 | 
				
			||||||
  border-radius: 10px;
 | 
					 | 
				
			||||||
  margin-bottom: 10px;
 | 
					 | 
				
			||||||
  box-shadow: var(--shadow);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .prompt-hint {
 | 
					 | 
				
			||||||
    color: var(--black);
 | 
					 | 
				
			||||||
    padding: 6px 10px;
 | 
					 | 
				
			||||||
    animation: slide-in ease 0.3s;
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
    transition: all ease 0.3s;
 | 
					 | 
				
			||||||
    border: transparent 1px solid;
 | 
					 | 
				
			||||||
    margin: 4px;
 | 
					 | 
				
			||||||
    border-radius: 8px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:not(:last-child) {
 | 
					 | 
				
			||||||
      margin-top: 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .hint-title {
 | 
					 | 
				
			||||||
      font-size: 12px;
 | 
					 | 
				
			||||||
      font-weight: bolder;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      @include single-line();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .hint-content {
 | 
					 | 
				
			||||||
      font-size: 12px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      @include single-line();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-selected,
 | 
					 | 
				
			||||||
    &:hover {
 | 
					 | 
				
			||||||
      border-color: var(--primary);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-input-panel-inner {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex: 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-input {
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  border-radius: 10px;
 | 
					 | 
				
			||||||
  border: var(--border-in-light);
 | 
					 | 
				
			||||||
  box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
 | 
					 | 
				
			||||||
  background-color: var(--white);
 | 
					 | 
				
			||||||
  color: var(--black);
 | 
					 | 
				
			||||||
  font-family: inherit;
 | 
					 | 
				
			||||||
  padding: 10px 90px 10px 14px;
 | 
					 | 
				
			||||||
  resize: none;
 | 
					 | 
				
			||||||
  outline: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-input:focus {
 | 
					 | 
				
			||||||
  border: 1px solid var(--primary);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.chat-input-send {
 | 
					 | 
				
			||||||
  background-color: var(--primary);
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  right: 30px;
 | 
					 | 
				
			||||||
  bottom: 32px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media only screen and (max-width: 600px) {
 | 
					 | 
				
			||||||
  .chat-input {
 | 
					 | 
				
			||||||
    font-size: 16px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .chat-input-send {
 | 
					 | 
				
			||||||
    bottom: 30px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.export-content {
 | 
					 | 
				
			||||||
  white-space: break-spaces;
 | 
					 | 
				
			||||||
  padding: 10px !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.loading-content {
 | 
					.loading-content {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
@@ -570,3 +343,7 @@
 | 
				
			|||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rtl-screen {
 | 
				
			||||||
 | 
					  direction: rtl;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
require("../polyfill");
 | 
					require("../polyfill");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useState, useEffect } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					 | 
				
			||||||
import styles from "./home.module.scss";
 | 
					import styles from "./home.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					import BotIcon from "../icons/bot.svg";
 | 
				
			||||||
@@ -15,24 +14,36 @@ import dynamic from "next/dynamic";
 | 
				
			|||||||
import { Path, SlotID } from "../constant";
 | 
					import { Path, SlotID } from "../constant";
 | 
				
			||||||
import { ErrorBoundary } from "./error";
 | 
					import { ErrorBoundary } from "./error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getISOLang, getLang } from "../locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  HashRouter as Router,
 | 
					  HashRouter as Router,
 | 
				
			||||||
  Routes,
 | 
					 | 
				
			||||||
  Route,
 | 
					  Route,
 | 
				
			||||||
 | 
					  Routes,
 | 
				
			||||||
  useLocation,
 | 
					  useLocation,
 | 
				
			||||||
} from "react-router-dom";
 | 
					} from "react-router-dom";
 | 
				
			||||||
import { SideBar } from "./sidebar";
 | 
					import { SideBar } from "./sidebar";
 | 
				
			||||||
import { useAppConfig } from "../store/config";
 | 
					import { useAppConfig } from "../store/config";
 | 
				
			||||||
 | 
					import { AuthPage } from "./auth";
 | 
				
			||||||
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
 | 
					import { type ClientApi, getClientApi } from "../client/api";
 | 
				
			||||||
 | 
					import { useAccessStore } from "../store";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Loading(props: { noLogo?: boolean }) {
 | 
					export function Loading(props: { noLogo?: boolean }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["loading-content"] + " no-dark"}>
 | 
					    <div className={clsx("no-dark", styles["loading-content"])}>
 | 
				
			||||||
      {!props.noLogo && <BotIcon />}
 | 
					      {!props.noLogo && <BotIcon />}
 | 
				
			||||||
      <LoadingIcon />
 | 
					      <LoadingIcon />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
 | 
				
			||||||
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
 | 
					const Settings = dynamic(async () => (await import("./settings")).Settings, {
 | 
				
			||||||
  loading: () => <Loading noLogo />,
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -49,6 +60,28 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
 | 
				
			|||||||
  loading: () => <Loading noLogo />,
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
 | 
				
			||||||
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SearchChat = dynamic(
 | 
				
			||||||
 | 
					  async () => (await import("./search-chat")).SearchChatPage,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Sd = dynamic(async () => (await import("./sd")).Sd, {
 | 
				
			||||||
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const McpMarketPage = dynamic(
 | 
				
			||||||
 | 
					  async () => (await import("./mcp-market")).McpMarketPage,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useSwitchTheme() {
 | 
					export function useSwitchTheme() {
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -63,23 +96,34 @@ export function useSwitchTheme() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const metaDescriptionDark = document.querySelector(
 | 
					    const metaDescriptionDark = document.querySelector(
 | 
				
			||||||
      'meta[name="theme-color"][media]',
 | 
					      'meta[name="theme-color"][media*="dark"]',
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const metaDescriptionLight = document.querySelector(
 | 
					    const metaDescriptionLight = document.querySelector(
 | 
				
			||||||
      'meta[name="theme-color"]:not([media])',
 | 
					      'meta[name="theme-color"][media*="light"]',
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (config.theme === "auto") {
 | 
					    if (config.theme === "auto") {
 | 
				
			||||||
      metaDescriptionDark?.setAttribute("content", "#151515");
 | 
					      metaDescriptionDark?.setAttribute("content", "#151515");
 | 
				
			||||||
      metaDescriptionLight?.setAttribute("content", "#fafafa");
 | 
					      metaDescriptionLight?.setAttribute("content", "#fafafa");
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      const themeColor = getCSSVar("--themeColor");
 | 
					      const themeColor = getCSSVar("--theme-color");
 | 
				
			||||||
      metaDescriptionDark?.setAttribute("content", themeColor);
 | 
					      metaDescriptionDark?.setAttribute("content", themeColor);
 | 
				
			||||||
      metaDescriptionLight?.setAttribute("content", themeColor);
 | 
					      metaDescriptionLight?.setAttribute("content", themeColor);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [config.theme]);
 | 
					  }, [config.theme]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function useHtmlLang() {
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const lang = getISOLang();
 | 
				
			||||||
 | 
					    const htmlLang = document.documentElement.lang;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (lang !== htmlLang) {
 | 
				
			||||||
 | 
					      document.documentElement.lang = lang;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const useHasHydrated = () => {
 | 
					const useHasHydrated = () => {
 | 
				
			||||||
  const [hasHydrated, setHasHydrated] = useState<boolean>(false);
 | 
					  const [hasHydrated, setHasHydrated] = useState<boolean>(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -90,40 +134,129 @@ const useHasHydrated = () => {
 | 
				
			|||||||
  return hasHydrated;
 | 
					  return hasHydrated;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function Screen() {
 | 
					const loadAsyncGoogleFont = () => {
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const linkEl = document.createElement("link");
 | 
				
			||||||
  const location = useLocation();
 | 
					  const proxyFontUrl = "/google-fonts";
 | 
				
			||||||
  const isHome = location.pathname === Path.Home;
 | 
					  const remoteFontUrl = "https://fonts.googleapis.com";
 | 
				
			||||||
  const isMobileScreen = useMobileScreen();
 | 
					  const googleFontUrl =
 | 
				
			||||||
 | 
					    getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
 | 
				
			||||||
 | 
					  linkEl.rel = "stylesheet";
 | 
				
			||||||
 | 
					  linkEl.href =
 | 
				
			||||||
 | 
					    googleFontUrl +
 | 
				
			||||||
 | 
					    "/css2?family=" +
 | 
				
			||||||
 | 
					    encodeURIComponent("Noto Sans:wght@300;400;700;900") +
 | 
				
			||||||
 | 
					    "&display=swap";
 | 
				
			||||||
 | 
					  document.head.appendChild(linkEl);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function WindowContent(props: { children: React.ReactNode }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      className={
 | 
					 | 
				
			||||||
        styles.container +
 | 
					 | 
				
			||||||
        ` ${
 | 
					 | 
				
			||||||
          config.tightBorder && !isMobileScreen
 | 
					 | 
				
			||||||
            ? styles["tight-container"]
 | 
					 | 
				
			||||||
            : styles.container
 | 
					 | 
				
			||||||
        }`
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <SideBar className={isHome ? styles["sidebar-show"] : ""} />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
					    <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
				
			||||||
        <Routes>
 | 
					      {props?.children}
 | 
				
			||||||
          <Route path={Path.Home} element={<Chat />} />
 | 
					 | 
				
			||||||
          <Route path={Path.NewChat} element={<NewChat />} />
 | 
					 | 
				
			||||||
          <Route path={Path.Masks} element={<MaskPage />} />
 | 
					 | 
				
			||||||
          <Route path={Path.Chat} element={<Chat />} />
 | 
					 | 
				
			||||||
          <Route path={Path.Settings} element={<Settings />} />
 | 
					 | 
				
			||||||
        </Routes>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Screen() {
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const location = useLocation();
 | 
				
			||||||
 | 
					  const isArtifact = location.pathname.includes(Path.Artifacts);
 | 
				
			||||||
 | 
					  const isHome = location.pathname === Path.Home;
 | 
				
			||||||
 | 
					  const isAuth = location.pathname === Path.Auth;
 | 
				
			||||||
 | 
					  const isSd = location.pathname === Path.Sd;
 | 
				
			||||||
 | 
					  const isSdNew = location.pathname === Path.SdNew;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isMobileScreen = useMobileScreen();
 | 
				
			||||||
 | 
					  const shouldTightBorder =
 | 
				
			||||||
 | 
					    getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    loadAsyncGoogleFont();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isArtifact) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Routes>
 | 
				
			||||||
 | 
					        <Route path="/artifacts/:id" element={<Artifacts />} />
 | 
				
			||||||
 | 
					      </Routes>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const renderContent = () => {
 | 
				
			||||||
 | 
					    if (isAuth) return <AuthPage />;
 | 
				
			||||||
 | 
					    if (isSd) return <Sd />;
 | 
				
			||||||
 | 
					    if (isSdNew) return <Sd />;
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        <SideBar
 | 
				
			||||||
 | 
					          className={clsx({
 | 
				
			||||||
 | 
					            [styles["sidebar-show"]]: isHome,
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <WindowContent>
 | 
				
			||||||
 | 
					          <Routes>
 | 
				
			||||||
 | 
					            <Route path={Path.Home} element={<Chat />} />
 | 
				
			||||||
 | 
					            <Route path={Path.NewChat} element={<NewChat />} />
 | 
				
			||||||
 | 
					            <Route path={Path.Masks} element={<MaskPage />} />
 | 
				
			||||||
 | 
					            <Route path={Path.Plugins} element={<PluginPage />} />
 | 
				
			||||||
 | 
					            <Route path={Path.SearchChat} element={<SearchChat />} />
 | 
				
			||||||
 | 
					            <Route path={Path.Chat} element={<Chat />} />
 | 
				
			||||||
 | 
					            <Route path={Path.Settings} element={<Settings />} />
 | 
				
			||||||
 | 
					            <Route path={Path.McpMarket} element={<McpMarketPage />} />
 | 
				
			||||||
 | 
					          </Routes>
 | 
				
			||||||
 | 
					        </WindowContent>
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={clsx(styles.container, {
 | 
				
			||||||
 | 
					        [styles["tight-container"]]: shouldTightBorder,
 | 
				
			||||||
 | 
					        [styles["rtl-screen"]]: getLang() === "ar",
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {renderContent()}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useLoadData() {
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const api: ClientApi = getClientApi(config.modelConfig.providerName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    (async () => {
 | 
				
			||||||
 | 
					      const models = await api.llm.models();
 | 
				
			||||||
 | 
					      config.mergeModels(models);
 | 
				
			||||||
 | 
					    })();
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Home() {
 | 
					export function Home() {
 | 
				
			||||||
  useSwitchTheme();
 | 
					  useSwitchTheme();
 | 
				
			||||||
 | 
					  useLoadData();
 | 
				
			||||||
 | 
					  useHtmlLang();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    console.log("[Config] got config from build time", getClientConfig());
 | 
				
			||||||
 | 
					    useAccessStore.getState().fetch();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const initMcp = async () => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const enabled = await isMcpEnabled();
 | 
				
			||||||
 | 
					        if (enabled) {
 | 
				
			||||||
 | 
					          console.log("[MCP] initializing...");
 | 
				
			||||||
 | 
					          await initializeMcpSystem();
 | 
				
			||||||
 | 
					          console.log("[MCP] initialized");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (err) {
 | 
				
			||||||
 | 
					        console.error("[MCP] failed to initialize:", err);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    initMcp();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!useHasHydrated()) {
 | 
					  if (!useHasHydrated()) {
 | 
				
			||||||
    return <Loading />;
 | 
					    return <Loading />;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,13 @@
 | 
				
			|||||||
.input-range {
 | 
					.input-range {
 | 
				
			||||||
  border: var(--border-in-light);
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
  border-radius: 10px;
 | 
					  border-radius: 10px;
 | 
				
			||||||
  padding: 5px 15px 5px 10px;
 | 
					  padding: 5px 10px 5px 10px;
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  max-width: 40%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input[type="range"] {
 | 
				
			||||||
 | 
					    max-width: calc(100% - 34px);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import * as React from "react";
 | 
					import * as React from "react";
 | 
				
			||||||
import styles from "./input-range.module.scss";
 | 
					import styles from "./input-range.module.scss";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface InputRangeProps {
 | 
					interface InputRangeProps {
 | 
				
			||||||
  onChange: React.ChangeEventHandler<HTMLInputElement>;
 | 
					  onChange: React.ChangeEventHandler<HTMLInputElement>;
 | 
				
			||||||
@@ -9,6 +10,7 @@ interface InputRangeProps {
 | 
				
			|||||||
  min: string;
 | 
					  min: string;
 | 
				
			||||||
  max: string;
 | 
					  max: string;
 | 
				
			||||||
  step: string;
 | 
					  step: string;
 | 
				
			||||||
 | 
					  aria: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function InputRange({
 | 
					export function InputRange({
 | 
				
			||||||
@@ -19,11 +21,13 @@ export function InputRange({
 | 
				
			|||||||
  min,
 | 
					  min,
 | 
				
			||||||
  max,
 | 
					  max,
 | 
				
			||||||
  step,
 | 
					  step,
 | 
				
			||||||
 | 
					  aria,
 | 
				
			||||||
}: InputRangeProps) {
 | 
					}: InputRangeProps) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["input-range"] + ` ${className ?? ""}`}>
 | 
					    <div className={clsx(styles["input-range"], className)}>
 | 
				
			||||||
      {title || value}
 | 
					      {title || value}
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
 | 
					        aria-label={aria}
 | 
				
			||||||
        type="range"
 | 
					        type="range"
 | 
				
			||||||
        title={title}
 | 
					        title={title}
 | 
				
			||||||
        value={value}
 | 
					        value={value}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,82 +5,274 @@ import RemarkBreaks from "remark-breaks";
 | 
				
			|||||||
import RehypeKatex from "rehype-katex";
 | 
					import RehypeKatex from "rehype-katex";
 | 
				
			||||||
import RemarkGfm from "remark-gfm";
 | 
					import RemarkGfm from "remark-gfm";
 | 
				
			||||||
import RehypeHighlight from "rehype-highlight";
 | 
					import RehypeHighlight from "rehype-highlight";
 | 
				
			||||||
import { useRef, useState, RefObject, useEffect } from "react";
 | 
					import { useRef, useState, RefObject, useEffect, useMemo } from "react";
 | 
				
			||||||
import { copyToClipboard } from "../utils";
 | 
					import { copyToClipboard, useWindowSize } from "../utils";
 | 
				
			||||||
 | 
					import mermaid from "mermaid";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
					import LoadingIcon from "../icons/three-dots.svg";
 | 
				
			||||||
 | 
					import ReloadButtonIcon from "../icons/reload.svg";
 | 
				
			||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { useDebouncedCallback } from "use-debounce";
 | 
				
			||||||
 | 
					import { showImageModal, FullScreen } from "./ui-lib";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ArtifactsShareButton,
 | 
				
			||||||
 | 
					  HTMLPreview,
 | 
				
			||||||
 | 
					  HTMLPreviewHander,
 | 
				
			||||||
 | 
					} from "./artifacts";
 | 
				
			||||||
 | 
					import { useChatStore } from "../store";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useAppConfig } from "../store/config";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Mermaid(props: { code: string }) {
 | 
				
			||||||
 | 
					  const ref = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					  const [hasError, setHasError] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (props.code && ref.current) {
 | 
				
			||||||
 | 
					      mermaid
 | 
				
			||||||
 | 
					        .run({
 | 
				
			||||||
 | 
					          nodes: [ref.current],
 | 
				
			||||||
 | 
					          suppressErrors: true,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((e) => {
 | 
				
			||||||
 | 
					          setHasError(true);
 | 
				
			||||||
 | 
					          console.error("[Mermaid] ", e.message);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [props.code]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function viewSvgInNewWindow() {
 | 
				
			||||||
 | 
					    const svg = ref.current?.querySelector("svg");
 | 
				
			||||||
 | 
					    if (!svg) return;
 | 
				
			||||||
 | 
					    const text = new XMLSerializer().serializeToString(svg);
 | 
				
			||||||
 | 
					    const blob = new Blob([text], { type: "image/svg+xml" });
 | 
				
			||||||
 | 
					    showImageModal(URL.createObjectURL(blob));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (hasError) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={clsx("no-dark", "mermaid")}
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        cursor: "pointer",
 | 
				
			||||||
 | 
					        overflow: "auto",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      onClick={() => viewSvgInNewWindow()}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {props.code}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function PreCode(props: { children: any }) {
 | 
					export function PreCode(props: { children: any }) {
 | 
				
			||||||
  const ref = useRef<HTMLPreElement>(null);
 | 
					  const ref = useRef<HTMLPreElement>(null);
 | 
				
			||||||
 | 
					  const previewRef = useRef<HTMLPreviewHander>(null);
 | 
				
			||||||
 | 
					  const [mermaidCode, setMermaidCode] = useState("");
 | 
				
			||||||
 | 
					  const [htmlCode, setHtmlCode] = useState("");
 | 
				
			||||||
 | 
					  const { height } = useWindowSize();
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const renderArtifacts = useDebouncedCallback(() => {
 | 
				
			||||||
 | 
					    if (!ref.current) return;
 | 
				
			||||||
 | 
					    const mermaidDom = ref.current.querySelector("code.language-mermaid");
 | 
				
			||||||
 | 
					    if (mermaidDom) {
 | 
				
			||||||
 | 
					      setMermaidCode((mermaidDom as HTMLElement).innerText);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const htmlDom = ref.current.querySelector("code.language-html");
 | 
				
			||||||
 | 
					    const refText = ref.current.querySelector("code")?.innerText;
 | 
				
			||||||
 | 
					    if (htmlDom) {
 | 
				
			||||||
 | 
					      setHtmlCode((htmlDom as HTMLElement).innerText);
 | 
				
			||||||
 | 
					    } else if (
 | 
				
			||||||
 | 
					      refText?.startsWith("<!DOCTYPE") ||
 | 
				
			||||||
 | 
					      refText?.startsWith("<svg") ||
 | 
				
			||||||
 | 
					      refText?.startsWith("<?xml")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      setHtmlCode(refText);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, 600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const enableArtifacts =
 | 
				
			||||||
 | 
					    session.mask?.enableArtifacts !== false && config.enableArtifacts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //Wrap the paragraph for plain-text
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (ref.current) {
 | 
				
			||||||
 | 
					      const codeElements = ref.current.querySelectorAll(
 | 
				
			||||||
 | 
					        "code",
 | 
				
			||||||
 | 
					      ) as NodeListOf<HTMLElement>;
 | 
				
			||||||
 | 
					      const wrapLanguages = [
 | 
				
			||||||
 | 
					        "",
 | 
				
			||||||
 | 
					        "md",
 | 
				
			||||||
 | 
					        "markdown",
 | 
				
			||||||
 | 
					        "text",
 | 
				
			||||||
 | 
					        "txt",
 | 
				
			||||||
 | 
					        "plaintext",
 | 
				
			||||||
 | 
					        "tex",
 | 
				
			||||||
 | 
					        "latex",
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					      codeElements.forEach((codeElement) => {
 | 
				
			||||||
 | 
					        let languageClass = codeElement.className.match(/language-(\w+)/);
 | 
				
			||||||
 | 
					        let name = languageClass ? languageClass[1] : "";
 | 
				
			||||||
 | 
					        if (wrapLanguages.includes(name)) {
 | 
				
			||||||
 | 
					          codeElement.style.whiteSpace = "pre-wrap";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      setTimeout(renderArtifacts, 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
      <pre ref={ref}>
 | 
					      <pre ref={ref}>
 | 
				
			||||||
        <span
 | 
					        <span
 | 
				
			||||||
          className="copy-code-button"
 | 
					          className="copy-code-button"
 | 
				
			||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
            if (ref.current) {
 | 
					            if (ref.current) {
 | 
				
			||||||
            const code = ref.current.innerText;
 | 
					              copyToClipboard(
 | 
				
			||||||
            copyToClipboard(code);
 | 
					                ref.current.querySelector("code")?.innerText ?? "",
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        ></span>
 | 
					        ></span>
 | 
				
			||||||
        {props.children}
 | 
					        {props.children}
 | 
				
			||||||
      </pre>
 | 
					      </pre>
 | 
				
			||||||
 | 
					      {mermaidCode.length > 0 && (
 | 
				
			||||||
 | 
					        <Mermaid code={mermaidCode} key={mermaidCode} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {htmlCode.length > 0 && enableArtifacts && (
 | 
				
			||||||
 | 
					        <FullScreen className="no-dark html" right={70}>
 | 
				
			||||||
 | 
					          <ArtifactsShareButton
 | 
				
			||||||
 | 
					            style={{ position: "absolute", right: 20, top: 10 }}
 | 
				
			||||||
 | 
					            getCode={() => htmlCode}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            style={{ position: "absolute", right: 120, top: 10 }}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            icon={<ReloadButtonIcon />}
 | 
				
			||||||
 | 
					            shadow
 | 
				
			||||||
 | 
					            onClick={() => previewRef.current?.reload()}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <HTMLPreview
 | 
				
			||||||
 | 
					            ref={previewRef}
 | 
				
			||||||
 | 
					            code={htmlCode}
 | 
				
			||||||
 | 
					            autoHeight={!document.fullscreenElement}
 | 
				
			||||||
 | 
					            height={!document.fullscreenElement ? 600 : height}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </FullScreen>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Markdown(
 | 
					function CustomCode(props: { children: any; className?: string }) {
 | 
				
			||||||
  props: {
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
    content: string;
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
    loading?: boolean;
 | 
					  const config = useAppConfig();
 | 
				
			||||||
    fontSize?: number;
 | 
					  const enableCodeFold =
 | 
				
			||||||
    parentRef: RefObject<HTMLDivElement>;
 | 
					    session.mask?.enableCodeFold !== false && config.enableCodeFold;
 | 
				
			||||||
  } & React.DOMAttributes<HTMLDivElement>,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  const mdRef = useRef<HTMLDivElement>(null);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const parent = props.parentRef.current;
 | 
					  const ref = useRef<HTMLPreElement>(null);
 | 
				
			||||||
  const md = mdRef.current;
 | 
					  const [collapsed, setCollapsed] = useState(true);
 | 
				
			||||||
  const rendered = useRef(true); // disable lazy loading for bad ux
 | 
					  const [showToggle, setShowToggle] = useState(false);
 | 
				
			||||||
  const [counter, setCounter] = useState(0);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    // to triggr rerender
 | 
					    if (ref.current) {
 | 
				
			||||||
    setCounter(counter + 1);
 | 
					      const codeHeight = ref.current.scrollHeight;
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					      setShowToggle(codeHeight > 400);
 | 
				
			||||||
  }, [props.loading]);
 | 
					      ref.current.scrollTop = ref.current.scrollHeight;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const inView =
 | 
					 | 
				
			||||||
    rendered.current ||
 | 
					 | 
				
			||||||
    (() => {
 | 
					 | 
				
			||||||
      if (parent && md) {
 | 
					 | 
				
			||||||
        const parentBounds = parent.getBoundingClientRect();
 | 
					 | 
				
			||||||
        const mdBounds = md.getBoundingClientRect();
 | 
					 | 
				
			||||||
        const isInRange = (x: number) =>
 | 
					 | 
				
			||||||
          x <= parentBounds.bottom && x >= parentBounds.top;
 | 
					 | 
				
			||||||
        const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (inView) {
 | 
					 | 
				
			||||||
          rendered.current = true;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }, [props.children]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return inView;
 | 
					  const toggleCollapsed = () => {
 | 
				
			||||||
      }
 | 
					    setCollapsed((collapsed) => !collapsed);
 | 
				
			||||||
    })();
 | 
					  };
 | 
				
			||||||
 | 
					  const renderShowMoreButton = () => {
 | 
				
			||||||
  const shouldLoading = props.loading || !inView;
 | 
					    if (showToggle && enableCodeFold && collapsed) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
      className="markdown-body"
 | 
					          className={clsx("show-hide-button", {
 | 
				
			||||||
      style={{ fontSize: `${props.fontSize ?? 14}px` }}
 | 
					            collapsed,
 | 
				
			||||||
      ref={mdRef}
 | 
					            expanded: !collapsed,
 | 
				
			||||||
      onContextMenu={props.onContextMenu}
 | 
					          })}
 | 
				
			||||||
      onDoubleClickCapture={props.onDoubleClickCapture}
 | 
					 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
      {shouldLoading ? (
 | 
					          <button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
 | 
				
			||||||
        <LoadingIcon />
 | 
					        </div>
 | 
				
			||||||
      ) : (
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <code
 | 
				
			||||||
 | 
					        className={clsx(props?.className)}
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        style={{
 | 
				
			||||||
 | 
					          maxHeight: enableCodeFold && collapsed ? "400px" : "none",
 | 
				
			||||||
 | 
					          overflowY: "hidden",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {props.children}
 | 
				
			||||||
 | 
					      </code>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {renderShowMoreButton()}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function escapeBrackets(text: string) {
 | 
				
			||||||
 | 
					  const pattern =
 | 
				
			||||||
 | 
					    /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
 | 
				
			||||||
 | 
					  return text.replace(
 | 
				
			||||||
 | 
					    pattern,
 | 
				
			||||||
 | 
					    (match, codeBlock, squareBracket, roundBracket) => {
 | 
				
			||||||
 | 
					      if (codeBlock) {
 | 
				
			||||||
 | 
					        return codeBlock;
 | 
				
			||||||
 | 
					      } else if (squareBracket) {
 | 
				
			||||||
 | 
					        return `$$${squareBracket}$$`;
 | 
				
			||||||
 | 
					      } else if (roundBracket) {
 | 
				
			||||||
 | 
					        return `$${roundBracket}$`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return match;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function tryWrapHtmlCode(text: string) {
 | 
				
			||||||
 | 
					  // try add wrap html code (fixed: html codeblock include 2 newline)
 | 
				
			||||||
 | 
					  // ignore embed codeblock
 | 
				
			||||||
 | 
					  if (text.includes("```")) {
 | 
				
			||||||
 | 
					    return text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return text
 | 
				
			||||||
 | 
					    .replace(
 | 
				
			||||||
 | 
					      /([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
 | 
				
			||||||
 | 
					      (match, quoteStart, lang, newLine, doctype) => {
 | 
				
			||||||
 | 
					        return !quoteStart ? "\n```html\n" + doctype : match;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .replace(
 | 
				
			||||||
 | 
					      /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
 | 
				
			||||||
 | 
					      (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
 | 
				
			||||||
 | 
					        return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function _MarkDownContent(props: { content: string }) {
 | 
				
			||||||
 | 
					  const escapedContent = useMemo(() => {
 | 
				
			||||||
 | 
					    return tryWrapHtmlCode(escapeBrackets(props.content));
 | 
				
			||||||
 | 
					  }, [props.content]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
    <ReactMarkdown
 | 
					    <ReactMarkdown
 | 
				
			||||||
      remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
 | 
					      remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
 | 
				
			||||||
      rehypePlugins={[
 | 
					      rehypePlugins={[
 | 
				
			||||||
@@ -95,11 +287,65 @@ export function Markdown(
 | 
				
			|||||||
      ]}
 | 
					      ]}
 | 
				
			||||||
      components={{
 | 
					      components={{
 | 
				
			||||||
        pre: PreCode,
 | 
					        pre: PreCode,
 | 
				
			||||||
 | 
					        code: CustomCode,
 | 
				
			||||||
 | 
					        p: (pProps) => <p {...pProps} dir="auto" />,
 | 
				
			||||||
 | 
					        a: (aProps) => {
 | 
				
			||||||
 | 
					          const href = aProps.href || "";
 | 
				
			||||||
 | 
					          if (/\.(aac|mp3|opus|wav)$/.test(href)) {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <figure>
 | 
				
			||||||
 | 
					                <audio controls src={href}></audio>
 | 
				
			||||||
 | 
					              </figure>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <video controls width="99.9%">
 | 
				
			||||||
 | 
					                <source src={href} />
 | 
				
			||||||
 | 
					              </video>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          const isInternal = /^\/#/i.test(href);
 | 
				
			||||||
 | 
					          const target = isInternal ? "_self" : aProps.target ?? "_blank";
 | 
				
			||||||
 | 
					          return <a {...aProps} target={target} />;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
          linkTarget={"_blank"}
 | 
					 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
          {props.content}
 | 
					      {escapedContent}
 | 
				
			||||||
    </ReactMarkdown>
 | 
					    </ReactMarkdown>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MarkdownContent = React.memo(_MarkDownContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Markdown(
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    content: string;
 | 
				
			||||||
 | 
					    loading?: boolean;
 | 
				
			||||||
 | 
					    fontSize?: number;
 | 
				
			||||||
 | 
					    fontFamily?: string;
 | 
				
			||||||
 | 
					    parentRef?: RefObject<HTMLDivElement>;
 | 
				
			||||||
 | 
					    defaultShow?: boolean;
 | 
				
			||||||
 | 
					  } & React.DOMAttributes<HTMLDivElement>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const mdRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className="markdown-body"
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        fontSize: `${props.fontSize ?? 14}px`,
 | 
				
			||||||
 | 
					        fontFamily: props.fontFamily || "inherit",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      ref={mdRef}
 | 
				
			||||||
 | 
					      onContextMenu={props.onContextMenu}
 | 
				
			||||||
 | 
					      onDoubleClickCapture={props.onDoubleClickCapture}
 | 
				
			||||||
 | 
					      dir="auto"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {props.loading ? (
 | 
				
			||||||
 | 
					        <LoadingIcon />
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <MarkdownContent content={props.content} />
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,4 @@
 | 
				
			|||||||
@import "../styles/animation.scss";
 | 
					@import "../styles/animation.scss";
 | 
				
			||||||
 | 
					 | 
				
			||||||
@keyframes search-in {
 | 
					 | 
				
			||||||
  from {
 | 
					 | 
				
			||||||
    opacity: 0;
 | 
					 | 
				
			||||||
    transform: translateY(5vh) scaleX(0.5);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  to {
 | 
					 | 
				
			||||||
    opacity: 1;
 | 
					 | 
				
			||||||
    transform: translateY(0) scaleX(1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.mask-page {
 | 
					.mask-page {
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -23,8 +11,9 @@
 | 
				
			|||||||
    .mask-filter {
 | 
					    .mask-filter {
 | 
				
			||||||
      width: 100%;
 | 
					      width: 100%;
 | 
				
			||||||
      max-width: 100%;
 | 
					      max-width: 100%;
 | 
				
			||||||
      margin-bottom: 10px;
 | 
					      margin-bottom: 20px;
 | 
				
			||||||
      animation: search-in ease 0.3s;
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,8 +21,6 @@
 | 
				
			|||||||
        flex-grow: 1;
 | 
					        flex-grow: 1;
 | 
				
			||||||
        max-width: 100%;
 | 
					        max-width: 100%;
 | 
				
			||||||
        min-width: 0;
 | 
					        min-width: 0;
 | 
				
			||||||
        margin-bottom: 20px;
 | 
					 | 
				
			||||||
        animation: search-in ease 0.3s;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .mask-filter-lang {
 | 
					      .mask-filter-lang {
 | 
				
			||||||
@@ -45,10 +32,7 @@
 | 
				
			|||||||
        height: 100%;
 | 
					        height: 100%;
 | 
				
			||||||
        margin-left: 10px;
 | 
					        margin-left: 10px;
 | 
				
			||||||
        box-sizing: border-box;
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        min-width: 80px;
 | 
				
			||||||
        button {
 | 
					 | 
				
			||||||
          padding: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,27 +11,65 @@ import CloseIcon from "../icons/close.svg";
 | 
				
			|||||||
import DeleteIcon from "../icons/delete.svg";
 | 
					import DeleteIcon from "../icons/delete.svg";
 | 
				
			||||||
import EyeIcon from "../icons/eye.svg";
 | 
					import EyeIcon from "../icons/eye.svg";
 | 
				
			||||||
import CopyIcon from "../icons/copy.svg";
 | 
					import CopyIcon from "../icons/copy.svg";
 | 
				
			||||||
 | 
					import DragIcon from "../icons/drag.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
 | 
					import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
 | 
				
			||||||
import { Message, ModelConfig, ROLES, useChatStore } from "../store";
 | 
					import {
 | 
				
			||||||
import { Input, List, ListItem, Modal, Popover, showToast } from "./ui-lib";
 | 
					  ChatMessage,
 | 
				
			||||||
 | 
					  createMessage,
 | 
				
			||||||
 | 
					  ModelConfig,
 | 
				
			||||||
 | 
					  ModelType,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					} from "../store";
 | 
				
			||||||
 | 
					import { MultimodalContent, ROLES } from "../client/api";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Input,
 | 
				
			||||||
 | 
					  List,
 | 
				
			||||||
 | 
					  ListItem,
 | 
				
			||||||
 | 
					  Modal,
 | 
				
			||||||
 | 
					  Popover,
 | 
				
			||||||
 | 
					  Select,
 | 
				
			||||||
 | 
					  showConfirm,
 | 
				
			||||||
 | 
					} from "./ui-lib";
 | 
				
			||||||
import { Avatar, AvatarPicker } from "./emoji";
 | 
					import { Avatar, AvatarPicker } from "./emoji";
 | 
				
			||||||
import Locale, { AllLangs, Lang } from "../locales";
 | 
					import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import chatStyle from "./chat.module.scss";
 | 
					import chatStyle from "./chat.module.scss";
 | 
				
			||||||
import { useEffect, useState } from "react";
 | 
					import { useState } from "react";
 | 
				
			||||||
import { downloadAs } from "../utils";
 | 
					import {
 | 
				
			||||||
import { Updater } from "../api/openai/typing";
 | 
					  copyToClipboard,
 | 
				
			||||||
 | 
					  downloadAs,
 | 
				
			||||||
 | 
					  getMessageImages,
 | 
				
			||||||
 | 
					  readFromFile,
 | 
				
			||||||
 | 
					} from "../utils";
 | 
				
			||||||
 | 
					import { Updater } from "../typing";
 | 
				
			||||||
import { ModelConfigList } from "./model-config";
 | 
					import { ModelConfigList } from "./model-config";
 | 
				
			||||||
import { FileName, Path } from "../constant";
 | 
					import { FileName, Path } from "../constant";
 | 
				
			||||||
import { BUILTIN_MASK_STORE } from "../masks";
 | 
					import { BUILTIN_MASK_STORE } from "../masks";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DragDropContext,
 | 
				
			||||||
 | 
					  Droppable,
 | 
				
			||||||
 | 
					  Draggable,
 | 
				
			||||||
 | 
					  OnDragEndResponder,
 | 
				
			||||||
 | 
					} from "@hello-pangea/dnd";
 | 
				
			||||||
 | 
					import { getMessageTextContent } from "../utils";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function MaskAvatar(props: { mask: Mask }) {
 | 
					// drag and drop helper function
 | 
				
			||||||
  return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
 | 
					function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
 | 
				
			||||||
    <Avatar avatar={props.mask.avatar} />
 | 
					  const result = [...list];
 | 
				
			||||||
 | 
					  const [removed] = result.splice(startIndex, 1);
 | 
				
			||||||
 | 
					  result.splice(endIndex, 0, removed);
 | 
				
			||||||
 | 
					  return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
 | 
				
			||||||
 | 
					  return props.avatar !== DEFAULT_MASK_AVATAR ? (
 | 
				
			||||||
 | 
					    <Avatar avatar={props.avatar} />
 | 
				
			||||||
  ) : (
 | 
					  ) : (
 | 
				
			||||||
    <Avatar model={props.mask.modelConfig.model} />
 | 
					    <Avatar model={props.model} />
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,6 +78,7 @@ export function MaskConfig(props: {
 | 
				
			|||||||
  updateMask: Updater<Mask>;
 | 
					  updateMask: Updater<Mask>;
 | 
				
			||||||
  extraListItems?: JSX.Element;
 | 
					  extraListItems?: JSX.Element;
 | 
				
			||||||
  readonly?: boolean;
 | 
					  readonly?: boolean;
 | 
				
			||||||
 | 
					  shouldSyncFromGlobal?: boolean;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const [showPicker, setShowPicker] = useState(false);
 | 
					  const [showPicker, setShowPicker] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,9 +87,20 @@ export function MaskConfig(props: {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const config = { ...props.mask.modelConfig };
 | 
					    const config = { ...props.mask.modelConfig };
 | 
				
			||||||
    updater(config);
 | 
					    updater(config);
 | 
				
			||||||
    props.updateMask((mask) => (mask.modelConfig = config));
 | 
					    props.updateMask((mask) => {
 | 
				
			||||||
 | 
					      mask.modelConfig = config;
 | 
				
			||||||
 | 
					      // if user changed current session mask, it will disable auto sync
 | 
				
			||||||
 | 
					      mask.syncGlobalConfig = false;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const copyMaskLink = () => {
 | 
				
			||||||
 | 
					    const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`;
 | 
				
			||||||
 | 
					    copyToClipboard(maskLink);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const globalConfig = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <ContextPrompts
 | 
					      <ContextPrompts
 | 
				
			||||||
@@ -77,22 +127,123 @@ export function MaskConfig(props: {
 | 
				
			|||||||
            onClose={() => setShowPicker(false)}
 | 
					            onClose={() => setShowPicker(false)}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
 | 
					              tabIndex={0}
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.Avatar}
 | 
				
			||||||
              onClick={() => setShowPicker(true)}
 | 
					              onClick={() => setShowPicker(true)}
 | 
				
			||||||
              style={{ cursor: "pointer" }}
 | 
					              style={{ cursor: "pointer" }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <MaskAvatar mask={props.mask} />
 | 
					              <MaskAvatar
 | 
				
			||||||
 | 
					                avatar={props.mask.avatar}
 | 
				
			||||||
 | 
					                model={props.mask.modelConfig.model}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </Popover>
 | 
					          </Popover>
 | 
				
			||||||
        </ListItem>
 | 
					        </ListItem>
 | 
				
			||||||
        <ListItem title={Locale.Mask.Config.Name}>
 | 
					        <ListItem title={Locale.Mask.Config.Name}>
 | 
				
			||||||
          <input
 | 
					          <input
 | 
				
			||||||
 | 
					            aria-label={Locale.Mask.Config.Name}
 | 
				
			||||||
            type="text"
 | 
					            type="text"
 | 
				
			||||||
            value={props.mask.name}
 | 
					            value={props.mask.name}
 | 
				
			||||||
            onInput={(e) =>
 | 
					            onInput={(e) =>
 | 
				
			||||||
              props.updateMask((mask) => (mask.name = e.currentTarget.value))
 | 
					              props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                mask.name = e.currentTarget.value;
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          ></input>
 | 
					          ></input>
 | 
				
			||||||
        </ListItem>
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					        <ListItem
 | 
				
			||||||
 | 
					          title={Locale.Mask.Config.HideContext.Title}
 | 
				
			||||||
 | 
					          subTitle={Locale.Mask.Config.HideContext.SubTitle}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            aria-label={Locale.Mask.Config.HideContext.Title}
 | 
				
			||||||
 | 
					            type="checkbox"
 | 
				
			||||||
 | 
					            checked={props.mask.hideContext}
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                mask.hideContext = e.currentTarget.checked;
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          ></input>
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {globalConfig.enableArtifacts && (
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.Artifacts.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.Artifacts.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.Artifacts.Title}
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={props.mask.enableArtifacts !== false}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                  mask.enableArtifacts = e.currentTarget.checked;
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {globalConfig.enableCodeFold && (
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.CodeFold.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.CodeFold.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.CodeFold.Title}
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={props.mask.enableCodeFold !== false}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                  mask.enableCodeFold = e.currentTarget.checked;
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {!props.shouldSyncFromGlobal ? (
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.Share.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.Share.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              aria={Locale.Mask.Config.Share.Title}
 | 
				
			||||||
 | 
					              icon={<CopyIcon />}
 | 
				
			||||||
 | 
					              text={Locale.Mask.Config.Share.Action}
 | 
				
			||||||
 | 
					              onClick={copyMaskLink}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {props.shouldSyncFromGlobal ? (
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.Sync.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.Sync.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.Sync.Title}
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={props.mask.syncGlobalConfig}
 | 
				
			||||||
 | 
					              onChange={async (e) => {
 | 
				
			||||||
 | 
					                const checked = e.currentTarget.checked;
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                  checked &&
 | 
				
			||||||
 | 
					                  (await showConfirm(Locale.Mask.Config.Sync.Confirm))
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                  props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                    mask.syncGlobalConfig = checked;
 | 
				
			||||||
 | 
					                    mask.modelConfig = { ...globalConfig.modelConfig };
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                } else if (!checked) {
 | 
				
			||||||
 | 
					                  props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                    mask.syncGlobalConfig = checked;
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        ) : null}
 | 
				
			||||||
      </List>
 | 
					      </List>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <List>
 | 
					      <List>
 | 
				
			||||||
@@ -106,35 +257,27 @@ export function MaskConfig(props: {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ContextPrompts(props: {
 | 
					function ContextPromptItem(props: {
 | 
				
			||||||
  context: Message[];
 | 
					  index: number;
 | 
				
			||||||
  updateContext: (updater: (context: Message[]) => void) => void;
 | 
					  prompt: ChatMessage;
 | 
				
			||||||
 | 
					  update: (prompt: ChatMessage) => void;
 | 
				
			||||||
 | 
					  remove: () => void;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const context = props.context;
 | 
					  const [focusingInput, setFocusingInput] = useState(false);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const addContextPrompt = (prompt: Message) => {
 | 
					 | 
				
			||||||
    props.updateContext((context) => context.push(prompt));
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const removeContextPrompt = (i: number) => {
 | 
					 | 
				
			||||||
    props.updateContext((context) => context.splice(i, 1));
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const updateContextPrompt = (i: number, prompt: Message) => {
 | 
					 | 
				
			||||||
    props.updateContext((context) => (context[i] = prompt));
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={chatStyle["context-prompt-row"]}>
 | 
				
			||||||
 | 
					      {!focusingInput && (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
      <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
 | 
					          <div className={chatStyle["context-drag"]}>
 | 
				
			||||||
        {context.map((c, i) => (
 | 
					            <DragIcon />
 | 
				
			||||||
          <div className={chatStyle["context-prompt-row"]} key={i}>
 | 
					          </div>
 | 
				
			||||||
            <select
 | 
					          <Select
 | 
				
			||||||
              value={c.role}
 | 
					            value={props.prompt.role}
 | 
				
			||||||
            className={chatStyle["context-role"]}
 | 
					            className={chatStyle["context-role"]}
 | 
				
			||||||
            onChange={(e) =>
 | 
					            onChange={(e) =>
 | 
				
			||||||
                updateContextPrompt(i, {
 | 
					              props.update({
 | 
				
			||||||
                  ...c,
 | 
					                ...props.prompt,
 | 
				
			||||||
                role: e.target.value as any,
 | 
					                role: e.target.value as any,
 | 
				
			||||||
              })
 | 
					              })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -144,28 +287,134 @@ export function ContextPrompts(props: {
 | 
				
			|||||||
                {r}
 | 
					                {r}
 | 
				
			||||||
              </option>
 | 
					              </option>
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
            </select>
 | 
					          </Select>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      <Input
 | 
					      <Input
 | 
				
			||||||
              value={c.content}
 | 
					        value={getMessageTextContent(props.prompt)}
 | 
				
			||||||
        type="text"
 | 
					        type="text"
 | 
				
			||||||
        className={chatStyle["context-content"]}
 | 
					        className={chatStyle["context-content"]}
 | 
				
			||||||
              rows={1}
 | 
					        rows={focusingInput ? 5 : 1}
 | 
				
			||||||
 | 
					        onFocus={() => setFocusingInput(true)}
 | 
				
			||||||
 | 
					        onBlur={() => {
 | 
				
			||||||
 | 
					          setFocusingInput(false);
 | 
				
			||||||
 | 
					          // If the selection is not removed when the user loses focus, some
 | 
				
			||||||
 | 
					          // extensions like "Translate" will always display a floating bar
 | 
				
			||||||
 | 
					          window?.getSelection()?.removeAllRanges();
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
        onInput={(e) =>
 | 
					        onInput={(e) =>
 | 
				
			||||||
                updateContextPrompt(i, {
 | 
					          props.update({
 | 
				
			||||||
                  ...c,
 | 
					            ...props.prompt,
 | 
				
			||||||
            content: e.currentTarget.value as any,
 | 
					            content: e.currentTarget.value as any,
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					      {!focusingInput && (
 | 
				
			||||||
        <IconButton
 | 
					        <IconButton
 | 
				
			||||||
          icon={<DeleteIcon />}
 | 
					          icon={<DeleteIcon />}
 | 
				
			||||||
          className={chatStyle["context-delete-button"]}
 | 
					          className={chatStyle["context-delete-button"]}
 | 
				
			||||||
              onClick={() => removeContextPrompt(i)}
 | 
					          onClick={() => props.remove()}
 | 
				
			||||||
          bordered
 | 
					          bordered
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
        ))}
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ContextPrompts(props: {
 | 
				
			||||||
 | 
					  context: ChatMessage[];
 | 
				
			||||||
 | 
					  updateContext: (updater: (context: ChatMessage[]) => void) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const context = props.context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const addContextPrompt = (prompt: ChatMessage, i: number) => {
 | 
				
			||||||
 | 
					    props.updateContext((context) => context.splice(i, 0, prompt));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const removeContextPrompt = (i: number) => {
 | 
				
			||||||
 | 
					    props.updateContext((context) => context.splice(i, 1));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateContextPrompt = (i: number, prompt: ChatMessage) => {
 | 
				
			||||||
 | 
					    props.updateContext((context) => {
 | 
				
			||||||
 | 
					      const images = getMessageImages(context[i]);
 | 
				
			||||||
 | 
					      context[i] = prompt;
 | 
				
			||||||
 | 
					      if (images.length > 0) {
 | 
				
			||||||
 | 
					        const text = getMessageTextContent(context[i]);
 | 
				
			||||||
 | 
					        const newContext: MultimodalContent[] = [{ type: "text", text }];
 | 
				
			||||||
 | 
					        for (const img of images) {
 | 
				
			||||||
 | 
					          newContext.push({ type: "image_url", image_url: { url: img } });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        context[i].content = newContext;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onDragEnd: OnDragEndResponder = (result) => {
 | 
				
			||||||
 | 
					    if (!result.destination) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const newContext = reorder(
 | 
				
			||||||
 | 
					      context,
 | 
				
			||||||
 | 
					      result.source.index,
 | 
				
			||||||
 | 
					      result.destination.index,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    props.updateContext((context) => {
 | 
				
			||||||
 | 
					      context.splice(0, context.length, ...newContext);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
 | 
				
			||||||
 | 
					        <DragDropContext onDragEnd={onDragEnd}>
 | 
				
			||||||
 | 
					          <Droppable droppableId="context-prompt-list">
 | 
				
			||||||
 | 
					            {(provided) => (
 | 
				
			||||||
 | 
					              <div ref={provided.innerRef} {...provided.droppableProps}>
 | 
				
			||||||
 | 
					                {context.map((c, i) => (
 | 
				
			||||||
 | 
					                  <Draggable
 | 
				
			||||||
 | 
					                    draggableId={c.id || i.toString()}
 | 
				
			||||||
 | 
					                    index={i}
 | 
				
			||||||
 | 
					                    key={c.id}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {(provided) => (
 | 
				
			||||||
 | 
					                      <div
 | 
				
			||||||
 | 
					                        ref={provided.innerRef}
 | 
				
			||||||
 | 
					                        {...provided.draggableProps}
 | 
				
			||||||
 | 
					                        {...provided.dragHandleProps}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <ContextPromptItem
 | 
				
			||||||
 | 
					                          index={i}
 | 
				
			||||||
 | 
					                          prompt={c}
 | 
				
			||||||
 | 
					                          update={(prompt) => updateContextPrompt(i, prompt)}
 | 
				
			||||||
 | 
					                          remove={() => removeContextPrompt(i)}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                        <div
 | 
				
			||||||
 | 
					                          className={chatStyle["context-prompt-insert"]}
 | 
				
			||||||
 | 
					                          onClick={() => {
 | 
				
			||||||
 | 
					                            addContextPrompt(
 | 
				
			||||||
 | 
					                              createMessage({
 | 
				
			||||||
 | 
					                                role: "user",
 | 
				
			||||||
 | 
					                                content: "",
 | 
				
			||||||
 | 
					                                date: new Date().toLocaleString(),
 | 
				
			||||||
 | 
					                              }),
 | 
				
			||||||
 | 
					                              i + 1,
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                          }}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          <AddIcon />
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  </Draggable>
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					                {provided.placeholder}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </Droppable>
 | 
				
			||||||
 | 
					        </DragDropContext>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {props.context.length === 0 && (
 | 
				
			||||||
          <div className={chatStyle["context-prompt-row"]}>
 | 
					          <div className={chatStyle["context-prompt-row"]}>
 | 
				
			||||||
            <IconButton
 | 
					            <IconButton
 | 
				
			||||||
              icon={<AddIcon />}
 | 
					              icon={<AddIcon />}
 | 
				
			||||||
@@ -173,14 +422,18 @@ export function ContextPrompts(props: {
 | 
				
			|||||||
              bordered
 | 
					              bordered
 | 
				
			||||||
              className={chatStyle["context-prompt-button"]}
 | 
					              className={chatStyle["context-prompt-button"]}
 | 
				
			||||||
              onClick={() =>
 | 
					              onClick={() =>
 | 
				
			||||||
              addContextPrompt({
 | 
					                addContextPrompt(
 | 
				
			||||||
                role: "system",
 | 
					                  createMessage({
 | 
				
			||||||
 | 
					                    role: "user",
 | 
				
			||||||
                    content: "",
 | 
					                    content: "",
 | 
				
			||||||
                    date: "",
 | 
					                    date: "",
 | 
				
			||||||
              })
 | 
					                  }),
 | 
				
			||||||
 | 
					                  props.context.length,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
@@ -192,7 +445,7 @@ export function MaskPage() {
 | 
				
			|||||||
  const maskStore = useMaskStore();
 | 
					  const maskStore = useMaskStore();
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [filterLang, setFilterLang] = useState<Lang>();
 | 
					  const filterLang = maskStore.language;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allMasks = maskStore
 | 
					  const allMasks = maskStore
 | 
				
			||||||
    .getAll()
 | 
					    .getAll()
 | 
				
			||||||
@@ -202,24 +455,46 @@ export function MaskPage() {
 | 
				
			|||||||
  const [searchText, setSearchText] = useState("");
 | 
					  const [searchText, setSearchText] = useState("");
 | 
				
			||||||
  const masks = searchText.length > 0 ? searchMasks : allMasks;
 | 
					  const masks = searchText.length > 0 ? searchMasks : allMasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // simple search, will refactor later
 | 
					  // refactored already, now it accurate
 | 
				
			||||||
  const onSearch = (text: string) => {
 | 
					  const onSearch = (text: string) => {
 | 
				
			||||||
    setSearchText(text);
 | 
					    setSearchText(text);
 | 
				
			||||||
    if (text.length > 0) {
 | 
					    if (text.length > 0) {
 | 
				
			||||||
      const result = allMasks.filter((m) => m.name.includes(text));
 | 
					      const result = allMasks.filter((m) =>
 | 
				
			||||||
 | 
					        m.name.toLowerCase().includes(text.toLowerCase()),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      setSearchMasks(result);
 | 
					      setSearchMasks(result);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      setSearchMasks(allMasks);
 | 
					      setSearchMasks(allMasks);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
 | 
					  const [editingMaskId, setEditingMaskId] = useState<string | undefined>();
 | 
				
			||||||
  const editingMask =
 | 
					  const editingMask =
 | 
				
			||||||
    maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
 | 
					    maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
 | 
				
			||||||
  const closeMaskModal = () => setEditingMaskId(undefined);
 | 
					  const closeMaskModal = () => setEditingMaskId(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const downloadAll = () => {
 | 
					  const downloadAll = () => {
 | 
				
			||||||
    downloadAs(JSON.stringify(masks), FileName.Masks);
 | 
					    downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const importFromFile = () => {
 | 
				
			||||||
 | 
					    readFromFile().then((content) => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const importMasks = JSON.parse(content);
 | 
				
			||||||
 | 
					        if (Array.isArray(importMasks)) {
 | 
				
			||||||
 | 
					          for (const mask of importMasks) {
 | 
				
			||||||
 | 
					            if (mask.name) {
 | 
				
			||||||
 | 
					              maskStore.create(mask);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        //if the content is a single mask.
 | 
				
			||||||
 | 
					        if (importMasks.name) {
 | 
				
			||||||
 | 
					          maskStore.create(importMasks);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {}
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -241,13 +516,15 @@ export function MaskPage() {
 | 
				
			|||||||
                icon={<DownloadIcon />}
 | 
					                icon={<DownloadIcon />}
 | 
				
			||||||
                bordered
 | 
					                bordered
 | 
				
			||||||
                onClick={downloadAll}
 | 
					                onClick={downloadAll}
 | 
				
			||||||
 | 
					                text={Locale.UI.Export}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div className="window-action-button">
 | 
					            <div className="window-action-button">
 | 
				
			||||||
              <IconButton
 | 
					              <IconButton
 | 
				
			||||||
                icon={<UploadIcon />}
 | 
					                icon={<UploadIcon />}
 | 
				
			||||||
 | 
					                text={Locale.UI.Import}
 | 
				
			||||||
                bordered
 | 
					                bordered
 | 
				
			||||||
                onClick={() => showToast(Locale.WIP)}
 | 
					                onClick={() => importFromFile()}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div className="window-action-button">
 | 
					            <div className="window-action-button">
 | 
				
			||||||
@@ -269,15 +546,15 @@ export function MaskPage() {
 | 
				
			|||||||
              autoFocus
 | 
					              autoFocus
 | 
				
			||||||
              onInput={(e) => onSearch(e.currentTarget.value)}
 | 
					              onInput={(e) => onSearch(e.currentTarget.value)}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <select
 | 
					            <Select
 | 
				
			||||||
              className={styles["mask-filter-lang"]}
 | 
					              className={styles["mask-filter-lang"]}
 | 
				
			||||||
              value={filterLang ?? Locale.Settings.Lang.All}
 | 
					              value={filterLang ?? Locale.Settings.Lang.All}
 | 
				
			||||||
              onChange={(e) => {
 | 
					              onChange={(e) => {
 | 
				
			||||||
                const value = e.currentTarget.value;
 | 
					                const value = e.currentTarget.value;
 | 
				
			||||||
                if (value === Locale.Settings.Lang.All) {
 | 
					                if (value === Locale.Settings.Lang.All) {
 | 
				
			||||||
                  setFilterLang(undefined);
 | 
					                  maskStore.setLanguage(undefined);
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                  setFilterLang(value as Lang);
 | 
					                  maskStore.setLanguage(value as Lang);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
@@ -286,33 +563,35 @@ export function MaskPage() {
 | 
				
			|||||||
              </option>
 | 
					              </option>
 | 
				
			||||||
              {AllLangs.map((lang) => (
 | 
					              {AllLangs.map((lang) => (
 | 
				
			||||||
                <option value={lang} key={lang}>
 | 
					                <option value={lang} key={lang}>
 | 
				
			||||||
                  {Locale.Settings.Lang.Options[lang]}
 | 
					                  {ALL_LANG_OPTIONS[lang]}
 | 
				
			||||||
                </option>
 | 
					                </option>
 | 
				
			||||||
              ))}
 | 
					              ))}
 | 
				
			||||||
            </select>
 | 
					            </Select>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div className={styles["mask-create"]}>
 | 
					 | 
				
			||||||
            <IconButton
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              className={styles["mask-create"]}
 | 
				
			||||||
              icon={<AddIcon />}
 | 
					              icon={<AddIcon />}
 | 
				
			||||||
              text={Locale.Mask.Page.Create}
 | 
					              text={Locale.Mask.Page.Create}
 | 
				
			||||||
              bordered
 | 
					              bordered
 | 
				
			||||||
                onClick={() => maskStore.create()}
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                const createdMask = maskStore.create();
 | 
				
			||||||
 | 
					                setEditingMaskId(createdMask.id);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            {masks.map((m) => (
 | 
					            {masks.map((m) => (
 | 
				
			||||||
              <div className={styles["mask-item"]} key={m.id}>
 | 
					              <div className={styles["mask-item"]} key={m.id}>
 | 
				
			||||||
                <div className={styles["mask-header"]}>
 | 
					                <div className={styles["mask-header"]}>
 | 
				
			||||||
                  <div className={styles["mask-icon"]}>
 | 
					                  <div className={styles["mask-icon"]}>
 | 
				
			||||||
                    <MaskAvatar mask={m} />
 | 
					                    <MaskAvatar avatar={m.avatar} model={m.modelConfig.model} />
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div className={styles["mask-title"]}>
 | 
					                  <div className={styles["mask-title"]}>
 | 
				
			||||||
                    <div className={styles["mask-name"]}>{m.name}</div>
 | 
					                    <div className={styles["mask-name"]}>{m.name}</div>
 | 
				
			||||||
                    <div className={styles["mask-info"] + " one-line"}>
 | 
					                    <div className={clsx(styles["mask-info"], "one-line")}>
 | 
				
			||||||
                      {`${Locale.Mask.Item.Info(m.context.length)} / ${
 | 
					                      {`${Locale.Mask.Item.Info(m.context.length)} / ${
 | 
				
			||||||
                        Locale.Settings.Lang.Options[m.lang]
 | 
					                        ALL_LANG_OPTIONS[m.lang]
 | 
				
			||||||
                      } / ${m.modelConfig.model}`}
 | 
					                      } / ${m.modelConfig.model}`}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
@@ -343,8 +622,8 @@ export function MaskPage() {
 | 
				
			|||||||
                    <IconButton
 | 
					                    <IconButton
 | 
				
			||||||
                      icon={<DeleteIcon />}
 | 
					                      icon={<DeleteIcon />}
 | 
				
			||||||
                      text={Locale.Mask.Item.Delete}
 | 
					                      text={Locale.Mask.Item.Delete}
 | 
				
			||||||
                      onClick={() => {
 | 
					                      onClick={async () => {
 | 
				
			||||||
                        if (confirm(Locale.Mask.Item.DeleteConfirm)) {
 | 
					                        if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) {
 | 
				
			||||||
                          maskStore.delete(m.id);
 | 
					                          maskStore.delete(m.id);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                      }}
 | 
					                      }}
 | 
				
			||||||
@@ -368,6 +647,12 @@ export function MaskPage() {
 | 
				
			|||||||
                text={Locale.Mask.EditModal.Download}
 | 
					                text={Locale.Mask.EditModal.Download}
 | 
				
			||||||
                key="export"
 | 
					                key="export"
 | 
				
			||||||
                bordered
 | 
					                bordered
 | 
				
			||||||
 | 
					                onClick={() =>
 | 
				
			||||||
 | 
					                  downloadAs(
 | 
				
			||||||
 | 
					                    JSON.stringify(editingMask),
 | 
				
			||||||
 | 
					                    `${editingMask.name}.json`,
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
              />,
 | 
					              />,
 | 
				
			||||||
              <IconButton
 | 
					              <IconButton
 | 
				
			||||||
                key="copy"
 | 
					                key="copy"
 | 
				
			||||||
@@ -385,7 +670,7 @@ export function MaskPage() {
 | 
				
			|||||||
            <MaskConfig
 | 
					            <MaskConfig
 | 
				
			||||||
              mask={editingMask}
 | 
					              mask={editingMask}
 | 
				
			||||||
              updateMask={(updater) =>
 | 
					              updateMask={(updater) =>
 | 
				
			||||||
                maskStore.update(editingMaskId!, updater)
 | 
					                maskStore.updateMask(editingMaskId!, updater)
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              readonly={editingMask.builtin}
 | 
					              readonly={editingMask.builtin}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										657
									
								
								app/components/mcp-market.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										657
									
								
								app/components/mcp-market.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,657 @@
 | 
				
			|||||||
 | 
					@import "../styles/animation.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mcp-market-page {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .loading-indicator {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    color: var(--primary);
 | 
				
			||||||
 | 
					    margin-left: 8px;
 | 
				
			||||||
 | 
					    font-weight: normal;
 | 
				
			||||||
 | 
					    opacity: 0.8;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .mcp-market-page-body {
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .loading-container,
 | 
				
			||||||
 | 
					    .empty-container {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      min-height: 200px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .loading-text,
 | 
				
			||||||
 | 
					    .empty-text {
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      color: var(--black);
 | 
				
			||||||
 | 
					      opacity: 0.5;
 | 
				
			||||||
 | 
					      text-align: center;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .mcp-market-filter {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      max-width: 100%;
 | 
				
			||||||
 | 
					      margin-bottom: 20px;
 | 
				
			||||||
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .search-bar {
 | 
				
			||||||
 | 
					        flex-grow: 1;
 | 
				
			||||||
 | 
					        max-width: 100%;
 | 
				
			||||||
 | 
					        min-width: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .server-list {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 1px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .mcp-market-item {
 | 
				
			||||||
 | 
					      padding: 20px;
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					      transition: all 0.3s ease;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.disabled {
 | 
				
			||||||
 | 
					        opacity: 0.7;
 | 
				
			||||||
 | 
					        pointer-events: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:not(:last-child) {
 | 
				
			||||||
 | 
					        border-bottom: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:first-child {
 | 
				
			||||||
 | 
					        border-top-left-radius: 10px;
 | 
				
			||||||
 | 
					        border-top-right-radius: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:last-child {
 | 
				
			||||||
 | 
					        border-bottom-left-radius: 10px;
 | 
				
			||||||
 | 
					        border-bottom-right-radius: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.loading {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        &::after {
 | 
				
			||||||
 | 
					          content: "";
 | 
				
			||||||
 | 
					          position: absolute;
 | 
				
			||||||
 | 
					          top: 0;
 | 
				
			||||||
 | 
					          left: 0;
 | 
				
			||||||
 | 
					          right: 0;
 | 
				
			||||||
 | 
					          bottom: 0;
 | 
				
			||||||
 | 
					          background: linear-gradient(
 | 
				
			||||||
 | 
					            90deg,
 | 
				
			||||||
 | 
					            transparent,
 | 
				
			||||||
 | 
					            rgba(255, 255, 255, 0.2),
 | 
				
			||||||
 | 
					            transparent
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          background-size: 200% 100%;
 | 
				
			||||||
 | 
					          animation: loading-pulse 1.5s infinite;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .operation-status {
 | 
				
			||||||
 | 
					        display: inline-flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        margin-left: 10px;
 | 
				
			||||||
 | 
					        padding: 2px 8px;
 | 
				
			||||||
 | 
					        border-radius: 4px;
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					        background-color: #16a34a;
 | 
				
			||||||
 | 
					        color: #fff;
 | 
				
			||||||
 | 
					        animation: pulse 1.5s infinite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &[data-status="stopping"] {
 | 
				
			||||||
 | 
					          background-color: #9ca3af;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &[data-status="starting"] {
 | 
				
			||||||
 | 
					          background-color: #4ade80;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &[data-status="error"] {
 | 
				
			||||||
 | 
					          background-color: #f87171;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .mcp-market-header {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        justify-content: space-between;
 | 
				
			||||||
 | 
					        align-items: flex-start;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mcp-market-title {
 | 
				
			||||||
 | 
					          flex-grow: 1;
 | 
				
			||||||
 | 
					          margin-right: 20px;
 | 
				
			||||||
 | 
					          max-width: calc(100% - 300px);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mcp-market-name {
 | 
				
			||||||
 | 
					          font-size: 14px;
 | 
				
			||||||
 | 
					          font-weight: bold;
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          gap: 8px;
 | 
				
			||||||
 | 
					          margin-bottom: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          .server-status {
 | 
				
			||||||
 | 
					            display: inline-flex;
 | 
				
			||||||
 | 
					            align-items: center;
 | 
				
			||||||
 | 
					            margin-left: 10px;
 | 
				
			||||||
 | 
					            padding: 2px 8px;
 | 
				
			||||||
 | 
					            border-radius: 4px;
 | 
				
			||||||
 | 
					            font-size: 12px;
 | 
				
			||||||
 | 
					            background-color: #22c55e;
 | 
				
			||||||
 | 
					            color: #fff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &.error {
 | 
				
			||||||
 | 
					              background-color: #ef4444;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &.stopped {
 | 
				
			||||||
 | 
					              background-color: #6b7280;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &.initializing {
 | 
				
			||||||
 | 
					              background-color: #f59e0b;
 | 
				
			||||||
 | 
					              animation: pulse 1.5s infinite;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .error-message {
 | 
				
			||||||
 | 
					              margin-left: 4px;
 | 
				
			||||||
 | 
					              font-size: 12px;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .repo-link {
 | 
				
			||||||
 | 
					          color: var(--primary);
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          display: inline-flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          gap: 4px;
 | 
				
			||||||
 | 
					          text-decoration: none;
 | 
				
			||||||
 | 
					          opacity: 0.8;
 | 
				
			||||||
 | 
					          transition: opacity 0.2s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:hover {
 | 
				
			||||||
 | 
					            opacity: 1;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          svg {
 | 
				
			||||||
 | 
					            width: 14px;
 | 
				
			||||||
 | 
					            height: 14px;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .tags-container {
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          gap: 4px;
 | 
				
			||||||
 | 
					          flex-wrap: wrap;
 | 
				
			||||||
 | 
					          margin-bottom: 8px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .tag {
 | 
				
			||||||
 | 
					          background: var(--gray);
 | 
				
			||||||
 | 
					          color: var(--black);
 | 
				
			||||||
 | 
					          padding: 2px 6px;
 | 
				
			||||||
 | 
					          border-radius: 4px;
 | 
				
			||||||
 | 
					          font-size: 10px;
 | 
				
			||||||
 | 
					          opacity: 0.8;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mcp-market-info {
 | 
				
			||||||
 | 
					          color: var(--black);
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          overflow: hidden;
 | 
				
			||||||
 | 
					          text-overflow: ellipsis;
 | 
				
			||||||
 | 
					          white-space: nowrap;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mcp-market-actions {
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          gap: 12px;
 | 
				
			||||||
 | 
					          align-items: flex-start;
 | 
				
			||||||
 | 
					          flex-shrink: 0;
 | 
				
			||||||
 | 
					          min-width: 180px;
 | 
				
			||||||
 | 
					          justify-content: flex-end;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .array-input {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 12px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 16px;
 | 
				
			||||||
 | 
					    border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					    background-color: var(--white);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .array-input-item {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      gap: 8px;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      padding: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      input {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        padding: 8px 12px;
 | 
				
			||||||
 | 
					        background-color: var(--gray-50);
 | 
				
			||||||
 | 
					        border-radius: 6px;
 | 
				
			||||||
 | 
					        transition: all 0.3s ease;
 | 
				
			||||||
 | 
					        font-size: 13px;
 | 
				
			||||||
 | 
					        border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          background-color: var(--gray-100);
 | 
				
			||||||
 | 
					          border-color: var(--gray-300);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:focus {
 | 
				
			||||||
 | 
					          background-color: var(--white);
 | 
				
			||||||
 | 
					          border-color: var(--primary);
 | 
				
			||||||
 | 
					          outline: none;
 | 
				
			||||||
 | 
					          box-shadow: 0 0 0 2px var(--primary-10);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &::placeholder {
 | 
				
			||||||
 | 
					          color: var(--gray-300);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :global(.icon-button.add-path-button) {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      background-color: var(--primary);
 | 
				
			||||||
 | 
					      color: white;
 | 
				
			||||||
 | 
					      padding: 8px 12px;
 | 
				
			||||||
 | 
					      border-radius: 6px;
 | 
				
			||||||
 | 
					      transition: all 0.3s ease;
 | 
				
			||||||
 | 
					      margin-top: 8px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      border: none;
 | 
				
			||||||
 | 
					      height: 36px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover {
 | 
				
			||||||
 | 
					        background-color: var(--primary-dark);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      svg {
 | 
				
			||||||
 | 
					        width: 16px;
 | 
				
			||||||
 | 
					        height: 16px;
 | 
				
			||||||
 | 
					        margin-right: 4px;
 | 
				
			||||||
 | 
					        filter: brightness(2);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .path-list {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .path-item {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      gap: 10px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      input {
 | 
				
			||||||
 | 
					        flex: 1;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        max-width: 100%;
 | 
				
			||||||
 | 
					        padding: 10px;
 | 
				
			||||||
 | 
					        border: var(--border-in-light);
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        font-size: 14px;
 | 
				
			||||||
 | 
					        background-color: var(--white);
 | 
				
			||||||
 | 
					        color: var(--black);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          border-color: var(--gray-300);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:focus {
 | 
				
			||||||
 | 
					          border-color: var(--primary);
 | 
				
			||||||
 | 
					          outline: none;
 | 
				
			||||||
 | 
					          box-shadow: 0 0 0 2px var(--primary-10);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .browse-button {
 | 
				
			||||||
 | 
					        padding: 8px;
 | 
				
			||||||
 | 
					        border: var(--border-in-light);
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        background-color: transparent;
 | 
				
			||||||
 | 
					        color: var(--black-50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          border-color: var(--primary);
 | 
				
			||||||
 | 
					          color: var(--primary);
 | 
				
			||||||
 | 
					          background-color: transparent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        svg {
 | 
				
			||||||
 | 
					          width: 16px;
 | 
				
			||||||
 | 
					          height: 16px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .delete-button {
 | 
				
			||||||
 | 
					        padding: 8px;
 | 
				
			||||||
 | 
					        border: var(--border-in-light);
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        background-color: transparent;
 | 
				
			||||||
 | 
					        color: var(--black-50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          border-color: var(--danger);
 | 
				
			||||||
 | 
					          color: var(--danger);
 | 
				
			||||||
 | 
					          background-color: transparent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        svg {
 | 
				
			||||||
 | 
					          width: 16px;
 | 
				
			||||||
 | 
					          height: 16px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .file-input {
 | 
				
			||||||
 | 
					        display: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .add-button {
 | 
				
			||||||
 | 
					      align-self: flex-start;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      gap: 5px;
 | 
				
			||||||
 | 
					      padding: 8px 12px;
 | 
				
			||||||
 | 
					      background-color: transparent;
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      color: var(--black);
 | 
				
			||||||
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					      margin-top: 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover {
 | 
				
			||||||
 | 
					        border-color: var(--primary);
 | 
				
			||||||
 | 
					        color: var(--primary);
 | 
				
			||||||
 | 
					        background-color: transparent;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      svg {
 | 
				
			||||||
 | 
					        width: 16px;
 | 
				
			||||||
 | 
					        height: 16px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .config-section {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .config-header {
 | 
				
			||||||
 | 
					      margin-bottom: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .config-title {
 | 
				
			||||||
 | 
					        font-size: 14px;
 | 
				
			||||||
 | 
					        font-weight: 600;
 | 
				
			||||||
 | 
					        color: var(--black);
 | 
				
			||||||
 | 
					        text-transform: capitalize;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .config-description {
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					        color: var(--gray-500);
 | 
				
			||||||
 | 
					        margin-top: 4px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .array-input {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 12px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      padding: 16px;
 | 
				
			||||||
 | 
					      border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .array-input-item {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        gap: 8px;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        padding: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        input {
 | 
				
			||||||
 | 
					          width: 100%;
 | 
				
			||||||
 | 
					          padding: 8px 12px;
 | 
				
			||||||
 | 
					          background-color: var(--gray-50);
 | 
				
			||||||
 | 
					          border-radius: 6px;
 | 
				
			||||||
 | 
					          transition: all 0.3s ease;
 | 
				
			||||||
 | 
					          font-size: 13px;
 | 
				
			||||||
 | 
					          border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:hover {
 | 
				
			||||||
 | 
					            background-color: var(--gray-100);
 | 
				
			||||||
 | 
					            border-color: var(--gray-300);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:focus {
 | 
				
			||||||
 | 
					            background-color: var(--white);
 | 
				
			||||||
 | 
					            border-color: var(--primary);
 | 
				
			||||||
 | 
					            outline: none;
 | 
				
			||||||
 | 
					            box-shadow: 0 0 0 2px var(--primary-10);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &::placeholder {
 | 
				
			||||||
 | 
					            color: var(--gray-300);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :global(.icon-button) {
 | 
				
			||||||
 | 
					          width: 32px;
 | 
				
			||||||
 | 
					          height: 32px;
 | 
				
			||||||
 | 
					          padding: 0;
 | 
				
			||||||
 | 
					          border-radius: 6px;
 | 
				
			||||||
 | 
					          background-color: transparent;
 | 
				
			||||||
 | 
					          border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					          flex-shrink: 0;
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:hover {
 | 
				
			||||||
 | 
					            background-color: var(--gray-100);
 | 
				
			||||||
 | 
					            border-color: var(--gray-300);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          svg {
 | 
				
			||||||
 | 
					            width: 16px;
 | 
				
			||||||
 | 
					            height: 16px;
 | 
				
			||||||
 | 
					            opacity: 0.7;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      :global(.icon-button.add-path-button) {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        background-color: var(--primary);
 | 
				
			||||||
 | 
					        color: white;
 | 
				
			||||||
 | 
					        padding: 8px 12px;
 | 
				
			||||||
 | 
					        border-radius: 6px;
 | 
				
			||||||
 | 
					        transition: all 0.3s ease;
 | 
				
			||||||
 | 
					        margin-top: 8px;
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        justify-content: center;
 | 
				
			||||||
 | 
					        border: none;
 | 
				
			||||||
 | 
					        height: 36px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          background-color: var(--primary-dark);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        svg {
 | 
				
			||||||
 | 
					          width: 16px;
 | 
				
			||||||
 | 
					          height: 16px;
 | 
				
			||||||
 | 
					          margin-right: 4px;
 | 
				
			||||||
 | 
					          filter: brightness(2);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .input-item {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    input {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      box-sizing: border-box;
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					      color: var(--black);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover {
 | 
				
			||||||
 | 
					        border-color: var(--gray-300);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:focus {
 | 
				
			||||||
 | 
					        border-color: var(--primary);
 | 
				
			||||||
 | 
					        outline: none;
 | 
				
			||||||
 | 
					        box-shadow: 0 0 0 2px var(--primary-10);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &::placeholder {
 | 
				
			||||||
 | 
					        color: var(--gray-300) !important;
 | 
				
			||||||
 | 
					        opacity: 1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .tools-list {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 16px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    overflow-x: hidden;
 | 
				
			||||||
 | 
					    word-break: break-word;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .tool-item {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .tool-name {
 | 
				
			||||||
 | 
					        font-size: 14px;
 | 
				
			||||||
 | 
					        font-weight: 600;
 | 
				
			||||||
 | 
					        color: var(--black);
 | 
				
			||||||
 | 
					        margin-bottom: 8px;
 | 
				
			||||||
 | 
					        padding-left: 12px;
 | 
				
			||||||
 | 
					        border-left: 3px solid var(--primary);
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .tool-description {
 | 
				
			||||||
 | 
					        font-size: 13px;
 | 
				
			||||||
 | 
					        color: var(--gray-500);
 | 
				
			||||||
 | 
					        line-height: 1.6;
 | 
				
			||||||
 | 
					        padding-left: 15px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  :global {
 | 
				
			||||||
 | 
					    .modal-content {
 | 
				
			||||||
 | 
					      margin-top: 20px;
 | 
				
			||||||
 | 
					      max-width: 100%;
 | 
				
			||||||
 | 
					      overflow-x: hidden;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .list {
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .list-item {
 | 
				
			||||||
 | 
					      border: none;
 | 
				
			||||||
 | 
					      background-color: transparent;
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .list-header {
 | 
				
			||||||
 | 
					        margin-bottom: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .list-title {
 | 
				
			||||||
 | 
					          font-size: 14px;
 | 
				
			||||||
 | 
					          font-weight: bold;
 | 
				
			||||||
 | 
					          text-transform: capitalize;
 | 
				
			||||||
 | 
					          color: var(--black);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .list-sub-title {
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          color: var(--gray-500);
 | 
				
			||||||
 | 
					          margin-top: 4px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes loading-pulse {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    background-position: 200% 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    background-position: -200% 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes pulse {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    opacity: 0.6;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  50% {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    opacity: 0.6;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										755
									
								
								app/components/mcp-market.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										755
									
								
								app/components/mcp-market.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,755 @@
 | 
				
			|||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { ErrorBoundary } from "./error";
 | 
				
			||||||
 | 
					import styles from "./mcp-market.module.scss";
 | 
				
			||||||
 | 
					import EditIcon from "../icons/edit.svg";
 | 
				
			||||||
 | 
					import AddIcon from "../icons/add.svg";
 | 
				
			||||||
 | 
					import CloseIcon from "../icons/close.svg";
 | 
				
			||||||
 | 
					import DeleteIcon from "../icons/delete.svg";
 | 
				
			||||||
 | 
					import RestartIcon from "../icons/reload.svg";
 | 
				
			||||||
 | 
					import EyeIcon from "../icons/eye.svg";
 | 
				
			||||||
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
 | 
					import { List, ListItem, Modal, showToast } from "./ui-lib";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  addMcpServer,
 | 
				
			||||||
 | 
					  getClientsStatus,
 | 
				
			||||||
 | 
					  getClientTools,
 | 
				
			||||||
 | 
					  getMcpConfigFromFile,
 | 
				
			||||||
 | 
					  isMcpEnabled,
 | 
				
			||||||
 | 
					  pauseMcpServer,
 | 
				
			||||||
 | 
					  restartAllClients,
 | 
				
			||||||
 | 
					  resumeMcpServer,
 | 
				
			||||||
 | 
					} from "../mcp/actions";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ListToolsResponse,
 | 
				
			||||||
 | 
					  McpConfigData,
 | 
				
			||||||
 | 
					  PresetServer,
 | 
				
			||||||
 | 
					  ServerConfig,
 | 
				
			||||||
 | 
					  ServerStatusResponse,
 | 
				
			||||||
 | 
					} from "../mcp/types";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					import PlayIcon from "../icons/play.svg";
 | 
				
			||||||
 | 
					import StopIcon from "../icons/pause.svg";
 | 
				
			||||||
 | 
					import { Path } from "../constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ConfigProperty {
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  required?: boolean;
 | 
				
			||||||
 | 
					  minItems?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function McpMarketPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const [mcpEnabled, setMcpEnabled] = useState(false);
 | 
				
			||||||
 | 
					  const [searchText, setSearchText] = useState("");
 | 
				
			||||||
 | 
					  const [userConfig, setUserConfig] = useState<Record<string, any>>({});
 | 
				
			||||||
 | 
					  const [editingServerId, setEditingServerId] = useState<string | undefined>();
 | 
				
			||||||
 | 
					  const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
 | 
				
			||||||
 | 
					  const [viewingServerId, setViewingServerId] = useState<string | undefined>();
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
 | 
					  const [config, setConfig] = useState<McpConfigData>();
 | 
				
			||||||
 | 
					  const [clientStatuses, setClientStatuses] = useState<
 | 
				
			||||||
 | 
					    Record<string, ServerStatusResponse>
 | 
				
			||||||
 | 
					  >({});
 | 
				
			||||||
 | 
					  const [loadingPresets, setLoadingPresets] = useState(true);
 | 
				
			||||||
 | 
					  const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
 | 
				
			||||||
 | 
					  const [loadingStates, setLoadingStates] = useState<Record<string, string>>(
 | 
				
			||||||
 | 
					    {},
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查 MCP 是否启用
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const checkMcpStatus = async () => {
 | 
				
			||||||
 | 
					      const enabled = await isMcpEnabled();
 | 
				
			||||||
 | 
					      setMcpEnabled(enabled);
 | 
				
			||||||
 | 
					      if (!enabled) {
 | 
				
			||||||
 | 
					        navigate(Path.Home);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    checkMcpStatus();
 | 
				
			||||||
 | 
					  }, [navigate]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 添加状态轮询
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!mcpEnabled || !config) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updateStatuses = async () => {
 | 
				
			||||||
 | 
					      const statuses = await getClientsStatus();
 | 
				
			||||||
 | 
					      setClientStatuses(statuses);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 立即执行一次
 | 
				
			||||||
 | 
					    updateStatuses();
 | 
				
			||||||
 | 
					    // 每 1000ms 轮询一次
 | 
				
			||||||
 | 
					    const timer = setInterval(updateStatuses, 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => clearInterval(timer);
 | 
				
			||||||
 | 
					  }, [mcpEnabled, config]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 加载预设服务器
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const loadPresetServers = async () => {
 | 
				
			||||||
 | 
					      if (!mcpEnabled) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        setLoadingPresets(true);
 | 
				
			||||||
 | 
					        const response = await fetch("https://nextchat.club/mcp/list");
 | 
				
			||||||
 | 
					        if (!response.ok) {
 | 
				
			||||||
 | 
					          throw new Error("Failed to load preset servers");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const data = await response.json();
 | 
				
			||||||
 | 
					        setPresetServers(data?.data ?? []);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error("Failed to load preset servers:", error);
 | 
				
			||||||
 | 
					        showToast("Failed to load preset servers");
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        setLoadingPresets(false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    loadPresetServers();
 | 
				
			||||||
 | 
					  }, [mcpEnabled]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 加载初始状态
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const loadInitialState = async () => {
 | 
				
			||||||
 | 
					      if (!mcpEnabled) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        setIsLoading(true);
 | 
				
			||||||
 | 
					        const config = await getMcpConfigFromFile();
 | 
				
			||||||
 | 
					        setConfig(config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取所有客户端的状态
 | 
				
			||||||
 | 
					        const statuses = await getClientsStatus();
 | 
				
			||||||
 | 
					        setClientStatuses(statuses);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error("Failed to load initial state:", error);
 | 
				
			||||||
 | 
					        showToast("Failed to load initial state");
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        setIsLoading(false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    loadInitialState();
 | 
				
			||||||
 | 
					  }, [mcpEnabled]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 加载当前编辑服务器的配置
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!editingServerId || !config) return;
 | 
				
			||||||
 | 
					    const currentConfig = config.mcpServers[editingServerId];
 | 
				
			||||||
 | 
					    if (currentConfig) {
 | 
				
			||||||
 | 
					      // 从当前配置中提取用户配置
 | 
				
			||||||
 | 
					      const preset = presetServers.find((s) => s.id === editingServerId);
 | 
				
			||||||
 | 
					      if (preset?.configSchema) {
 | 
				
			||||||
 | 
					        const userConfig: Record<string, any> = {};
 | 
				
			||||||
 | 
					        Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
 | 
				
			||||||
 | 
					          if (mapping.type === "spread") {
 | 
				
			||||||
 | 
					            // For spread types, extract the array from args.
 | 
				
			||||||
 | 
					            const startPos = mapping.position ?? 0;
 | 
				
			||||||
 | 
					            userConfig[key] = currentConfig.args.slice(startPos);
 | 
				
			||||||
 | 
					          } else if (mapping.type === "single") {
 | 
				
			||||||
 | 
					            // For single types, get a single value
 | 
				
			||||||
 | 
					            userConfig[key] = currentConfig.args[mapping.position ?? 0];
 | 
				
			||||||
 | 
					          } else if (
 | 
				
			||||||
 | 
					            mapping.type === "env" &&
 | 
				
			||||||
 | 
					            mapping.key &&
 | 
				
			||||||
 | 
					            currentConfig.env
 | 
				
			||||||
 | 
					          ) {
 | 
				
			||||||
 | 
					            // For env types, get values from environment variables
 | 
				
			||||||
 | 
					            userConfig[key] = currentConfig.env[mapping.key];
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        setUserConfig(userConfig);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setUserConfig({});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [editingServerId, config, presetServers]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!mcpEnabled) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查服务器是否已添加
 | 
				
			||||||
 | 
					  const isServerAdded = (id: string) => {
 | 
				
			||||||
 | 
					    return id in (config?.mcpServers ?? {});
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 保存服务器配置
 | 
				
			||||||
 | 
					  const saveServerConfig = async () => {
 | 
				
			||||||
 | 
					    const preset = presetServers.find((s) => s.id === editingServerId);
 | 
				
			||||||
 | 
					    if (!preset || !preset.configSchema || !editingServerId) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const savingServerId = editingServerId;
 | 
				
			||||||
 | 
					    setEditingServerId(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      updateLoadingState(savingServerId, "Updating configuration...");
 | 
				
			||||||
 | 
					      // 构建服务器配置
 | 
				
			||||||
 | 
					      const args = [...preset.baseArgs];
 | 
				
			||||||
 | 
					      const env: Record<string, string> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
 | 
				
			||||||
 | 
					        const value = userConfig[key];
 | 
				
			||||||
 | 
					        if (mapping.type === "spread" && Array.isArray(value)) {
 | 
				
			||||||
 | 
					          const pos = mapping.position ?? 0;
 | 
				
			||||||
 | 
					          args.splice(pos, 0, ...value);
 | 
				
			||||||
 | 
					        } else if (
 | 
				
			||||||
 | 
					          mapping.type === "single" &&
 | 
				
			||||||
 | 
					          mapping.position !== undefined
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          args[mapping.position] = value;
 | 
				
			||||||
 | 
					        } else if (
 | 
				
			||||||
 | 
					          mapping.type === "env" &&
 | 
				
			||||||
 | 
					          mapping.key &&
 | 
				
			||||||
 | 
					          typeof value === "string"
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          env[mapping.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const serverConfig: ServerConfig = {
 | 
				
			||||||
 | 
					        command: preset.command,
 | 
				
			||||||
 | 
					        args,
 | 
				
			||||||
 | 
					        ...(Object.keys(env).length > 0 ? { env } : {}),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const newConfig = await addMcpServer(savingServerId, serverConfig);
 | 
				
			||||||
 | 
					      setConfig(newConfig);
 | 
				
			||||||
 | 
					      showToast("Server configuration updated successfully");
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast(
 | 
				
			||||||
 | 
					        error instanceof Error ? error.message : "Failed to save configuration",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      updateLoadingState(savingServerId, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 获取服务器支持的 Tools
 | 
				
			||||||
 | 
					  const loadTools = async (id: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const result = await getClientTools(id);
 | 
				
			||||||
 | 
					      if (result) {
 | 
				
			||||||
 | 
					        setTools(result);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        throw new Error("Failed to load tools");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast("Failed to load tools");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					      setTools(null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 更新加载状态的辅助函数
 | 
				
			||||||
 | 
					  const updateLoadingState = (id: string, message: string | null) => {
 | 
				
			||||||
 | 
					    setLoadingStates((prev) => {
 | 
				
			||||||
 | 
					      if (message === null) {
 | 
				
			||||||
 | 
					        const { [id]: _, ...rest } = prev;
 | 
				
			||||||
 | 
					        return rest;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return { ...prev, [id]: message };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 修改添加服务器函数
 | 
				
			||||||
 | 
					  const addServer = async (preset: PresetServer) => {
 | 
				
			||||||
 | 
					    if (!preset.configurable) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const serverId = preset.id;
 | 
				
			||||||
 | 
					        updateLoadingState(serverId, "Creating MCP client...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const serverConfig: ServerConfig = {
 | 
				
			||||||
 | 
					          command: preset.command,
 | 
				
			||||||
 | 
					          args: [...preset.baseArgs],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const newConfig = await addMcpServer(preset.id, serverConfig);
 | 
				
			||||||
 | 
					        setConfig(newConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 更新状态
 | 
				
			||||||
 | 
					        const statuses = await getClientsStatus();
 | 
				
			||||||
 | 
					        setClientStatuses(statuses);
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        updateLoadingState(preset.id, null);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // 如果需要配置,打开配置对话框
 | 
				
			||||||
 | 
					      setEditingServerId(preset.id);
 | 
				
			||||||
 | 
					      setUserConfig({});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 修改暂停服务器函数
 | 
				
			||||||
 | 
					  const pauseServer = async (id: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      updateLoadingState(id, "Stopping server...");
 | 
				
			||||||
 | 
					      const newConfig = await pauseMcpServer(id);
 | 
				
			||||||
 | 
					      setConfig(newConfig);
 | 
				
			||||||
 | 
					      showToast("Server stopped successfully");
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast("Failed to stop server");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      updateLoadingState(id, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Restart server
 | 
				
			||||||
 | 
					  const restartServer = async (id: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      updateLoadingState(id, "Starting server...");
 | 
				
			||||||
 | 
					      await resumeMcpServer(id);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast(
 | 
				
			||||||
 | 
					        error instanceof Error
 | 
				
			||||||
 | 
					          ? error.message
 | 
				
			||||||
 | 
					          : "Failed to start server, please check logs",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      updateLoadingState(id, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Restart all clients
 | 
				
			||||||
 | 
					  const handleRestartAll = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      updateLoadingState("all", "Restarting all servers...");
 | 
				
			||||||
 | 
					      const newConfig = await restartAllClients();
 | 
				
			||||||
 | 
					      setConfig(newConfig);
 | 
				
			||||||
 | 
					      showToast("Restarting all clients");
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast("Failed to restart clients");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      updateLoadingState("all", null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Render configuration form
 | 
				
			||||||
 | 
					  const renderConfigForm = () => {
 | 
				
			||||||
 | 
					    const preset = presetServers.find((s) => s.id === editingServerId);
 | 
				
			||||||
 | 
					    if (!preset?.configSchema) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Object.entries(preset.configSchema.properties).map(
 | 
				
			||||||
 | 
					      ([key, prop]: [string, ConfigProperty]) => {
 | 
				
			||||||
 | 
					        if (prop.type === "array") {
 | 
				
			||||||
 | 
					          const currentValue = userConfig[key as keyof typeof userConfig] || [];
 | 
				
			||||||
 | 
					          const itemLabel = (prop as any).itemLabel || key;
 | 
				
			||||||
 | 
					          const addButtonText =
 | 
				
			||||||
 | 
					            (prop as any).addButtonText || `Add ${itemLabel}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <ListItem
 | 
				
			||||||
 | 
					              key={key}
 | 
				
			||||||
 | 
					              title={key}
 | 
				
			||||||
 | 
					              subTitle={prop.description}
 | 
				
			||||||
 | 
					              vertical
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className={styles["path-list"]}>
 | 
				
			||||||
 | 
					                {(currentValue as string[]).map(
 | 
				
			||||||
 | 
					                  (value: string, index: number) => (
 | 
				
			||||||
 | 
					                    <div key={index} className={styles["path-item"]}>
 | 
				
			||||||
 | 
					                      <input
 | 
				
			||||||
 | 
					                        type="text"
 | 
				
			||||||
 | 
					                        value={value}
 | 
				
			||||||
 | 
					                        placeholder={`${itemLabel} ${index + 1}`}
 | 
				
			||||||
 | 
					                        onChange={(e) => {
 | 
				
			||||||
 | 
					                          const newValue = [...currentValue] as string[];
 | 
				
			||||||
 | 
					                          newValue[index] = e.target.value;
 | 
				
			||||||
 | 
					                          setUserConfig({ ...userConfig, [key]: newValue });
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                      <IconButton
 | 
				
			||||||
 | 
					                        icon={<DeleteIcon />}
 | 
				
			||||||
 | 
					                        className={styles["delete-button"]}
 | 
				
			||||||
 | 
					                        onClick={() => {
 | 
				
			||||||
 | 
					                          const newValue = [...currentValue] as string[];
 | 
				
			||||||
 | 
					                          newValue.splice(index, 1);
 | 
				
			||||||
 | 
					                          setUserConfig({ ...userConfig, [key]: newValue });
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  icon={<AddIcon />}
 | 
				
			||||||
 | 
					                  text={addButtonText}
 | 
				
			||||||
 | 
					                  className={styles["add-button"]}
 | 
				
			||||||
 | 
					                  bordered
 | 
				
			||||||
 | 
					                  onClick={() => {
 | 
				
			||||||
 | 
					                    const newValue = [...currentValue, ""] as string[];
 | 
				
			||||||
 | 
					                    setUserConfig({ ...userConfig, [key]: newValue });
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </ListItem>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        } else if (prop.type === "string") {
 | 
				
			||||||
 | 
					          const currentValue = userConfig[key as keyof typeof userConfig] || "";
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <ListItem key={key} title={key} subTitle={prop.description}>
 | 
				
			||||||
 | 
					              <input
 | 
				
			||||||
 | 
					                aria-label={key}
 | 
				
			||||||
 | 
					                type="text"
 | 
				
			||||||
 | 
					                value={currentValue}
 | 
				
			||||||
 | 
					                placeholder={`Enter ${key}`}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  setUserConfig({ ...userConfig, [key]: e.target.value });
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </ListItem>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const checkServerStatus = (clientId: string) => {
 | 
				
			||||||
 | 
					    return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getServerStatusDisplay = (clientId: string) => {
 | 
				
			||||||
 | 
					    const status = checkServerStatus(clientId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const statusMap = {
 | 
				
			||||||
 | 
					      undefined: null, // 未配置/未找到不显示
 | 
				
			||||||
 | 
					      // 添加初始化状态
 | 
				
			||||||
 | 
					      initializing: (
 | 
				
			||||||
 | 
					        <span className={clsx(styles["server-status"], styles["initializing"])}>
 | 
				
			||||||
 | 
					          Initializing
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      paused: (
 | 
				
			||||||
 | 
					        <span className={clsx(styles["server-status"], styles["stopped"])}>
 | 
				
			||||||
 | 
					          Stopped
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      active: <span className={styles["server-status"]}>Running</span>,
 | 
				
			||||||
 | 
					      error: (
 | 
				
			||||||
 | 
					        <span className={clsx(styles["server-status"], styles["error"])}>
 | 
				
			||||||
 | 
					          Error
 | 
				
			||||||
 | 
					          <span className={styles["error-message"]}>: {status.errorMsg}</span>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return statusMap[status.status];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get the type of operation status
 | 
				
			||||||
 | 
					  const getOperationStatusType = (message: string) => {
 | 
				
			||||||
 | 
					    if (message.toLowerCase().includes("stopping")) return "stopping";
 | 
				
			||||||
 | 
					    if (message.toLowerCase().includes("starting")) return "starting";
 | 
				
			||||||
 | 
					    if (message.toLowerCase().includes("error")) return "error";
 | 
				
			||||||
 | 
					    return "default";
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 渲染服务器列表
 | 
				
			||||||
 | 
					  const renderServerList = () => {
 | 
				
			||||||
 | 
					    if (loadingPresets) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className={styles["loading-container"]}>
 | 
				
			||||||
 | 
					          <div className={styles["loading-text"]}>
 | 
				
			||||||
 | 
					            Loading preset server list...
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!Array.isArray(presetServers) || presetServers.length === 0) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className={styles["empty-container"]}>
 | 
				
			||||||
 | 
					          <div className={styles["empty-text"]}>No servers available</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return presetServers
 | 
				
			||||||
 | 
					      .filter((server) => {
 | 
				
			||||||
 | 
					        if (searchText.length === 0) return true;
 | 
				
			||||||
 | 
					        const searchLower = searchText.toLowerCase();
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          server.name.toLowerCase().includes(searchLower) ||
 | 
				
			||||||
 | 
					          server.description.toLowerCase().includes(searchLower) ||
 | 
				
			||||||
 | 
					          server.tags.some((tag) => tag.toLowerCase().includes(searchLower))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .sort((a, b) => {
 | 
				
			||||||
 | 
					        const aStatus = checkServerStatus(a.id).status;
 | 
				
			||||||
 | 
					        const bStatus = checkServerStatus(b.id).status;
 | 
				
			||||||
 | 
					        const aLoading = loadingStates[a.id];
 | 
				
			||||||
 | 
					        const bLoading = loadingStates[b.id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 定义状态优先级
 | 
				
			||||||
 | 
					        const statusPriority: Record<string, number> = {
 | 
				
			||||||
 | 
					          error: 0, // Highest priority for error status
 | 
				
			||||||
 | 
					          active: 1, // Second for active
 | 
				
			||||||
 | 
					          initializing: 2, // Initializing
 | 
				
			||||||
 | 
					          starting: 3, // Starting
 | 
				
			||||||
 | 
					          stopping: 4, // Stopping
 | 
				
			||||||
 | 
					          paused: 5, // Paused
 | 
				
			||||||
 | 
					          undefined: 6, // Lowest priority for undefined
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get actual status (including loading status)
 | 
				
			||||||
 | 
					        const getEffectiveStatus = (status: string, loading?: string) => {
 | 
				
			||||||
 | 
					          if (loading) {
 | 
				
			||||||
 | 
					            const operationType = getOperationStatusType(loading);
 | 
				
			||||||
 | 
					            return operationType === "default" ? status : operationType;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (status === "initializing" && !loading) {
 | 
				
			||||||
 | 
					            return "active";
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return status;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading);
 | 
				
			||||||
 | 
					        const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 首先按状态排序
 | 
				
			||||||
 | 
					        if (aEffectiveStatus !== bEffectiveStatus) {
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            (statusPriority[aEffectiveStatus] ?? 6) -
 | 
				
			||||||
 | 
					            (statusPriority[bEffectiveStatus] ?? 6)
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Sort by name when statuses are the same
 | 
				
			||||||
 | 
					        return a.name.localeCompare(b.name);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .map((server) => (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={clsx(styles["mcp-market-item"], {
 | 
				
			||||||
 | 
					            [styles["loading"]]: loadingStates[server.id],
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					          key={server.id}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className={styles["mcp-market-header"]}>
 | 
				
			||||||
 | 
					            <div className={styles["mcp-market-title"]}>
 | 
				
			||||||
 | 
					              <div className={styles["mcp-market-name"]}>
 | 
				
			||||||
 | 
					                {server.name}
 | 
				
			||||||
 | 
					                {loadingStates[server.id] && (
 | 
				
			||||||
 | 
					                  <span
 | 
				
			||||||
 | 
					                    className={styles["operation-status"]}
 | 
				
			||||||
 | 
					                    data-status={getOperationStatusType(
 | 
				
			||||||
 | 
					                      loadingStates[server.id],
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {loadingStates[server.id]}
 | 
				
			||||||
 | 
					                  </span>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                {!loadingStates[server.id] && getServerStatusDisplay(server.id)}
 | 
				
			||||||
 | 
					                {server.repo && (
 | 
				
			||||||
 | 
					                  <a
 | 
				
			||||||
 | 
					                    href={server.repo}
 | 
				
			||||||
 | 
					                    target="_blank"
 | 
				
			||||||
 | 
					                    rel="noopener noreferrer"
 | 
				
			||||||
 | 
					                    className={styles["repo-link"]}
 | 
				
			||||||
 | 
					                    title="Open repository"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <GithubIcon />
 | 
				
			||||||
 | 
					                  </a>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div className={styles["tags-container"]}>
 | 
				
			||||||
 | 
					                {server.tags.map((tag, index) => (
 | 
				
			||||||
 | 
					                  <span key={index} className={styles["tag"]}>
 | 
				
			||||||
 | 
					                    {tag}
 | 
				
			||||||
 | 
					                  </span>
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={clsx(styles["mcp-market-info"], "one-line")}
 | 
				
			||||||
 | 
					                title={server.description}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {server.description}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className={styles["mcp-market-actions"]}>
 | 
				
			||||||
 | 
					              {isServerAdded(server.id) ? (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  {server.configurable && (
 | 
				
			||||||
 | 
					                    <IconButton
 | 
				
			||||||
 | 
					                      icon={<EditIcon />}
 | 
				
			||||||
 | 
					                      text="Configure"
 | 
				
			||||||
 | 
					                      onClick={() => setEditingServerId(server.id)}
 | 
				
			||||||
 | 
					                      disabled={isLoading}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                  {checkServerStatus(server.id).status === "paused" ? (
 | 
				
			||||||
 | 
					                    <>
 | 
				
			||||||
 | 
					                      <IconButton
 | 
				
			||||||
 | 
					                        icon={<PlayIcon />}
 | 
				
			||||||
 | 
					                        text="Start"
 | 
				
			||||||
 | 
					                        onClick={() => restartServer(server.id)}
 | 
				
			||||||
 | 
					                        disabled={isLoading}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                      {/* <IconButton
 | 
				
			||||||
 | 
					                        icon={<DeleteIcon />}
 | 
				
			||||||
 | 
					                        text="Remove"
 | 
				
			||||||
 | 
					                        onClick={() => removeServer(server.id)}
 | 
				
			||||||
 | 
					                        disabled={isLoading}
 | 
				
			||||||
 | 
					                      /> */}
 | 
				
			||||||
 | 
					                    </>
 | 
				
			||||||
 | 
					                  ) : (
 | 
				
			||||||
 | 
					                    <>
 | 
				
			||||||
 | 
					                      <IconButton
 | 
				
			||||||
 | 
					                        icon={<EyeIcon />}
 | 
				
			||||||
 | 
					                        text="Tools"
 | 
				
			||||||
 | 
					                        onClick={async () => {
 | 
				
			||||||
 | 
					                          setViewingServerId(server.id);
 | 
				
			||||||
 | 
					                          await loadTools(server.id);
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                        disabled={
 | 
				
			||||||
 | 
					                          isLoading ||
 | 
				
			||||||
 | 
					                          checkServerStatus(server.id).status === "error"
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                      <IconButton
 | 
				
			||||||
 | 
					                        icon={<StopIcon />}
 | 
				
			||||||
 | 
					                        text="Stop"
 | 
				
			||||||
 | 
					                        onClick={() => pauseServer(server.id)}
 | 
				
			||||||
 | 
					                        disabled={isLoading}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    </>
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  icon={<AddIcon />}
 | 
				
			||||||
 | 
					                  text="Add"
 | 
				
			||||||
 | 
					                  onClick={() => addServer(server)}
 | 
				
			||||||
 | 
					                  disabled={isLoading}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ErrorBoundary>
 | 
				
			||||||
 | 
					      <div className={styles["mcp-market-page"]}>
 | 
				
			||||||
 | 
					        <div className="window-header">
 | 
				
			||||||
 | 
					          <div className="window-header-title">
 | 
				
			||||||
 | 
					            <div className="window-header-main-title">
 | 
				
			||||||
 | 
					              MCP Market
 | 
				
			||||||
 | 
					              {loadingStates["all"] && (
 | 
				
			||||||
 | 
					                <span className={styles["loading-indicator"]}>
 | 
				
			||||||
 | 
					                  {loadingStates["all"]}
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="window-header-sub-title">
 | 
				
			||||||
 | 
					              {Object.keys(config?.mcpServers ?? {}).length} servers configured
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="window-actions">
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<RestartIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                onClick={handleRestartAll}
 | 
				
			||||||
 | 
					                text="Restart All"
 | 
				
			||||||
 | 
					                disabled={isLoading}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<CloseIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                onClick={() => navigate(-1)}
 | 
				
			||||||
 | 
					                disabled={isLoading}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className={styles["mcp-market-page-body"]}>
 | 
				
			||||||
 | 
					          <div className={styles["mcp-market-filter"]}>
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              className={styles["search-bar"]}
 | 
				
			||||||
 | 
					              placeholder={"Search MCP Server"}
 | 
				
			||||||
 | 
					              autoFocus
 | 
				
			||||||
 | 
					              onInput={(e) => setSearchText(e.currentTarget.value)}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className={styles["server-list"]}>{renderServerList()}</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {/*编辑服务器配置*/}
 | 
				
			||||||
 | 
					        {editingServerId && (
 | 
				
			||||||
 | 
					          <div className="modal-mask">
 | 
				
			||||||
 | 
					            <Modal
 | 
				
			||||||
 | 
					              title={`Configure Server - ${editingServerId}`}
 | 
				
			||||||
 | 
					              onClose={() => !isLoading && setEditingServerId(undefined)}
 | 
				
			||||||
 | 
					              actions={[
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  key="cancel"
 | 
				
			||||||
 | 
					                  text="Cancel"
 | 
				
			||||||
 | 
					                  onClick={() => setEditingServerId(undefined)}
 | 
				
			||||||
 | 
					                  bordered
 | 
				
			||||||
 | 
					                  disabled={isLoading}
 | 
				
			||||||
 | 
					                />,
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  key="confirm"
 | 
				
			||||||
 | 
					                  text="Save"
 | 
				
			||||||
 | 
					                  type="primary"
 | 
				
			||||||
 | 
					                  onClick={saveServerConfig}
 | 
				
			||||||
 | 
					                  bordered
 | 
				
			||||||
 | 
					                  disabled={isLoading}
 | 
				
			||||||
 | 
					                />,
 | 
				
			||||||
 | 
					              ]}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <List>{renderConfigForm()}</List>
 | 
				
			||||||
 | 
					            </Modal>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {viewingServerId && (
 | 
				
			||||||
 | 
					          <div className="modal-mask">
 | 
				
			||||||
 | 
					            <Modal
 | 
				
			||||||
 | 
					              title={`Server Details - ${viewingServerId}`}
 | 
				
			||||||
 | 
					              onClose={() => setViewingServerId(undefined)}
 | 
				
			||||||
 | 
					              actions={[
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  key="close"
 | 
				
			||||||
 | 
					                  text="Close"
 | 
				
			||||||
 | 
					                  onClick={() => setViewingServerId(undefined)}
 | 
				
			||||||
 | 
					                  bordered
 | 
				
			||||||
 | 
					                />,
 | 
				
			||||||
 | 
					              ]}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className={styles["tools-list"]}>
 | 
				
			||||||
 | 
					                {isLoading ? (
 | 
				
			||||||
 | 
					                  <div>Loading...</div>
 | 
				
			||||||
 | 
					                ) : tools?.tools ? (
 | 
				
			||||||
 | 
					                  tools.tools.map(
 | 
				
			||||||
 | 
					                    (tool: ListToolsResponse["tools"], index: number) => (
 | 
				
			||||||
 | 
					                      <div key={index} className={styles["tool-item"]}>
 | 
				
			||||||
 | 
					                        <div className={styles["tool-name"]}>{tool.name}</div>
 | 
				
			||||||
 | 
					                        <div className={styles["tool-description"]}>
 | 
				
			||||||
 | 
					                          {tool.description}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <div>No tools available</div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </Modal>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </ErrorBoundary>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										82
									
								
								app/components/message-selector.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								app/components/message-selector.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					.message-selector {
 | 
				
			||||||
 | 
					  .message-filter {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .search-bar {
 | 
				
			||||||
 | 
					      max-width: unset;
 | 
				
			||||||
 | 
					      flex-grow: 1;
 | 
				
			||||||
 | 
					      margin-right: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .actions {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      button:not(:last-child) {
 | 
				
			||||||
 | 
					        margin-right: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .search-bar {
 | 
				
			||||||
 | 
					        margin-right: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .actions {
 | 
				
			||||||
 | 
					        margin-top: 20px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        button {
 | 
				
			||||||
 | 
					          flex-grow: 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .messages {
 | 
				
			||||||
 | 
					    margin-top: 20px;
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					    border: var(--border-in-light);
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .message {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      padding: 8px 10px;
 | 
				
			||||||
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &-selected {
 | 
				
			||||||
 | 
					        background-color: var(--second);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:not(:last-child) {
 | 
				
			||||||
 | 
					        border-bottom: var(--border-in-light);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .avatar {
 | 
				
			||||||
 | 
					        margin-right: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .body {
 | 
				
			||||||
 | 
					        flex: 1;
 | 
				
			||||||
 | 
					        max-width: calc(100% - 80px);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .date {
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          line-height: 1.2;
 | 
				
			||||||
 | 
					          opacity: 0.5;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .content {
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .checkbox {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        justify-content: flex-end;
 | 
				
			||||||
 | 
					        flex: 1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										238
									
								
								app/components/message-selector.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								app/components/message-selector.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
				
			|||||||
 | 
					import { useEffect, useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import { ChatMessage, useAppConfig, useChatStore } from "../store";
 | 
				
			||||||
 | 
					import { Updater } from "../typing";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { Avatar } from "./emoji";
 | 
				
			||||||
 | 
					import { MaskAvatar } from "./mask";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import styles from "./message-selector.module.scss";
 | 
				
			||||||
 | 
					import { getMessageTextContent } from "../utils";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function useShiftRange() {
 | 
				
			||||||
 | 
					  const [startIndex, setStartIndex] = useState<number>();
 | 
				
			||||||
 | 
					  const [endIndex, setEndIndex] = useState<number>();
 | 
				
			||||||
 | 
					  const [shiftDown, setShiftDown] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onClickIndex = (index: number) => {
 | 
				
			||||||
 | 
					    if (shiftDown && startIndex !== undefined) {
 | 
				
			||||||
 | 
					      setEndIndex(index);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setStartIndex(index);
 | 
				
			||||||
 | 
					      setEndIndex(undefined);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const onKeyDown = (e: KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (e.key !== "Shift") return;
 | 
				
			||||||
 | 
					      setShiftDown(true);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const onKeyUp = (e: KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (e.key !== "Shift") return;
 | 
				
			||||||
 | 
					      setShiftDown(false);
 | 
				
			||||||
 | 
					      setStartIndex(undefined);
 | 
				
			||||||
 | 
					      setEndIndex(undefined);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener("keyup", onKeyUp);
 | 
				
			||||||
 | 
					    window.addEventListener("keydown", onKeyDown);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      window.removeEventListener("keyup", onKeyUp);
 | 
				
			||||||
 | 
					      window.removeEventListener("keydown", onKeyDown);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    onClickIndex,
 | 
				
			||||||
 | 
					    startIndex,
 | 
				
			||||||
 | 
					    endIndex,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useMessageSelector() {
 | 
				
			||||||
 | 
					  const [selection, setSelection] = useState(new Set<string>());
 | 
				
			||||||
 | 
					  const updateSelection: Updater<Set<string>> = (updater) => {
 | 
				
			||||||
 | 
					    const newSelection = new Set<string>(selection);
 | 
				
			||||||
 | 
					    updater(newSelection);
 | 
				
			||||||
 | 
					    setSelection(newSelection);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    selection,
 | 
				
			||||||
 | 
					    updateSelection,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function MessageSelector(props: {
 | 
				
			||||||
 | 
					  selection: Set<string>;
 | 
				
			||||||
 | 
					  updateSelection: Updater<Set<string>>;
 | 
				
			||||||
 | 
					  defaultSelectAll?: boolean;
 | 
				
			||||||
 | 
					  onSelected?: (messages: ChatMessage[]) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const LATEST_COUNT = 4;
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
 | 
				
			||||||
 | 
					  const allMessages = useMemo(() => {
 | 
				
			||||||
 | 
					    let startIndex = Math.max(0, session.clearContextIndex ?? 0);
 | 
				
			||||||
 | 
					    if (startIndex === session.messages.length - 1) {
 | 
				
			||||||
 | 
					      startIndex = 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return session.messages.slice(startIndex);
 | 
				
			||||||
 | 
					  }, [session.messages, session.clearContextIndex]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const messages = useMemo(
 | 
				
			||||||
 | 
					    () =>
 | 
				
			||||||
 | 
					      allMessages.filter(
 | 
				
			||||||
 | 
					        (m, i) =>
 | 
				
			||||||
 | 
					          m.id && // message must have id
 | 
				
			||||||
 | 
					          isValid(m) &&
 | 
				
			||||||
 | 
					          (i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    [allMessages],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const messageCount = messages.length;
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [searchInput, setSearchInput] = useState("");
 | 
				
			||||||
 | 
					  const [searchIds, setSearchIds] = useState(new Set<string>());
 | 
				
			||||||
 | 
					  const isInSearchResult = (id: string) => {
 | 
				
			||||||
 | 
					    return searchInput.length === 0 || searchIds.has(id);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const doSearch = (text: string) => {
 | 
				
			||||||
 | 
					    const searchResults = new Set<string>();
 | 
				
			||||||
 | 
					    if (text.length > 0) {
 | 
				
			||||||
 | 
					      messages.forEach((m) =>
 | 
				
			||||||
 | 
					        getMessageTextContent(m).includes(text)
 | 
				
			||||||
 | 
					          ? searchResults.add(m.id!)
 | 
				
			||||||
 | 
					          : null,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setSearchIds(searchResults);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // for range selection
 | 
				
			||||||
 | 
					  const { startIndex, endIndex, onClickIndex } = useShiftRange();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const selectAll = () => {
 | 
				
			||||||
 | 
					    props.updateSelection((selection) =>
 | 
				
			||||||
 | 
					      messages.forEach((m) => selection.add(m.id!)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (props.defaultSelectAll) {
 | 
				
			||||||
 | 
					      selectAll();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (startIndex === undefined || endIndex === undefined) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const [start, end] = [startIndex, endIndex].sort((a, b) => a - b);
 | 
				
			||||||
 | 
					    props.updateSelection((selection) => {
 | 
				
			||||||
 | 
					      for (let i = start; i <= end; i += 1) {
 | 
				
			||||||
 | 
					        selection.add(messages[i].id ?? i);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [startIndex, endIndex]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["message-selector"]}>
 | 
				
			||||||
 | 
					      <div className={styles["message-filter"]}>
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          placeholder={Locale.Select.Search}
 | 
				
			||||||
 | 
					          className={clsx(styles["filter-item"], styles["search-bar"])}
 | 
				
			||||||
 | 
					          value={searchInput}
 | 
				
			||||||
 | 
					          onInput={(e) => {
 | 
				
			||||||
 | 
					            setSearchInput(e.currentTarget.value);
 | 
				
			||||||
 | 
					            doSearch(e.currentTarget.value);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        ></input>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className={styles["actions"]}>
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            text={Locale.Select.All}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            className={styles["filter-item"]}
 | 
				
			||||||
 | 
					            onClick={selectAll}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            text={Locale.Select.Latest}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            className={styles["filter-item"]}
 | 
				
			||||||
 | 
					            onClick={() =>
 | 
				
			||||||
 | 
					              props.updateSelection((selection) => {
 | 
				
			||||||
 | 
					                selection.clear();
 | 
				
			||||||
 | 
					                messages
 | 
				
			||||||
 | 
					                  .slice(messageCount - LATEST_COUNT)
 | 
				
			||||||
 | 
					                  .forEach((m) => selection.add(m.id!));
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            text={Locale.Select.Clear}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            className={styles["filter-item"]}
 | 
				
			||||||
 | 
					            onClick={() =>
 | 
				
			||||||
 | 
					              props.updateSelection((selection) => selection.clear())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className={styles["messages"]}>
 | 
				
			||||||
 | 
					        {messages.map((m, i) => {
 | 
				
			||||||
 | 
					          if (!isInSearchResult(m.id!)) return null;
 | 
				
			||||||
 | 
					          const id = m.id ?? i;
 | 
				
			||||||
 | 
					          const isSelected = props.selection.has(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              className={clsx(styles["message"], {
 | 
				
			||||||
 | 
					                [styles["message-selected"]]: props.selection.has(m.id!),
 | 
				
			||||||
 | 
					              })}
 | 
				
			||||||
 | 
					              key={i}
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                props.updateSelection((selection) => {
 | 
				
			||||||
 | 
					                  selection.has(id) ? selection.delete(id) : selection.add(id);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                onClickIndex(i);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className={styles["avatar"]}>
 | 
				
			||||||
 | 
					                {m.role === "user" ? (
 | 
				
			||||||
 | 
					                  <Avatar avatar={config.avatar}></Avatar>
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <MaskAvatar
 | 
				
			||||||
 | 
					                    avatar={session.mask.avatar}
 | 
				
			||||||
 | 
					                    model={m.model || session.mask.modelConfig.model}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div className={styles["body"]}>
 | 
				
			||||||
 | 
					                <div className={styles["date"]}>
 | 
				
			||||||
 | 
					                  {new Date(m.date).toLocaleString()}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div className={clsx(styles["content"], "one-line")}>
 | 
				
			||||||
 | 
					                  {getMessageTextContent(m)}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div className={styles["checkbox"]}>
 | 
				
			||||||
 | 
					                <input type="checkbox" checked={isSelected} readOnly></input>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								app/components/model-config.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/components/model-config.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					.select-compress-model {
 | 
				
			||||||
 | 
					  width: 60%;
 | 
				
			||||||
 | 
					  select {
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    white-space: normal;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,40 +1,60 @@
 | 
				
			|||||||
import styles from "./settings.module.scss";
 | 
					import { ServiceProvider } from "@/app/constant";
 | 
				
			||||||
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
 | 
					import { ModalConfigValidator, ModelConfig } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { InputRange } from "./input-range";
 | 
					import { InputRange } from "./input-range";
 | 
				
			||||||
import { List, ListItem } from "./ui-lib";
 | 
					import { ListItem, Select } from "./ui-lib";
 | 
				
			||||||
 | 
					import { useAllModels } from "../utils/hooks";
 | 
				
			||||||
 | 
					import { groupBy } from "lodash-es";
 | 
				
			||||||
 | 
					import styles from "./model-config.module.scss";
 | 
				
			||||||
 | 
					import { getModelProvider } from "../utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ModelConfigList(props: {
 | 
					export function ModelConfigList(props: {
 | 
				
			||||||
  modelConfig: ModelConfig;
 | 
					  modelConfig: ModelConfig;
 | 
				
			||||||
  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
					  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
 | 
					  const allModels = useAllModels();
 | 
				
			||||||
 | 
					  const groupModels = groupBy(
 | 
				
			||||||
 | 
					    allModels.filter((v) => v.available),
 | 
				
			||||||
 | 
					    "provider.providerName",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
 | 
				
			||||||
 | 
					  const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <ListItem title={Locale.Settings.Model}>
 | 
					      <ListItem title={Locale.Settings.Model}>
 | 
				
			||||||
        <select
 | 
					        <Select
 | 
				
			||||||
          value={props.modelConfig.model}
 | 
					          aria-label={Locale.Settings.Model}
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          align="left"
 | 
				
			||||||
          onChange={(e) => {
 | 
					          onChange={(e) => {
 | 
				
			||||||
            props.updateConfig(
 | 
					            const [model, providerName] = getModelProvider(
 | 
				
			||||||
              (config) =>
 | 
					 | 
				
			||||||
                (config.model = ModalConfigValidator.model(
 | 
					 | 
				
			||||||
              e.currentTarget.value,
 | 
					              e.currentTarget.value,
 | 
				
			||||||
                )),
 | 
					 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					            props.updateConfig((config) => {
 | 
				
			||||||
 | 
					              config.model = ModalConfigValidator.model(model);
 | 
				
			||||||
 | 
					              config.providerName = providerName as ServiceProvider;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {ALL_MODELS.map((v) => (
 | 
					          {Object.keys(groupModels).map((providerName, index) => (
 | 
				
			||||||
            <option value={v.name} key={v.name} disabled={!v.available}>
 | 
					            <optgroup label={providerName} key={index}>
 | 
				
			||||||
              {v.name}
 | 
					              {groupModels[providerName].map((v, i) => (
 | 
				
			||||||
 | 
					                <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
 | 
				
			||||||
 | 
					                  {v.displayName}
 | 
				
			||||||
                </option>
 | 
					                </option>
 | 
				
			||||||
              ))}
 | 
					              ))}
 | 
				
			||||||
        </select>
 | 
					            </optgroup>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </Select>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
      <ListItem
 | 
					      <ListItem
 | 
				
			||||||
        title={Locale.Settings.Temperature.Title}
 | 
					        title={Locale.Settings.Temperature.Title}
 | 
				
			||||||
        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
					        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InputRange
 | 
					        <InputRange
 | 
				
			||||||
 | 
					          aria={Locale.Settings.Temperature.Title}
 | 
				
			||||||
          value={props.modelConfig.temperature?.toFixed(1)}
 | 
					          value={props.modelConfig.temperature?.toFixed(1)}
 | 
				
			||||||
          min="0"
 | 
					          min="0"
 | 
				
			||||||
          max="1" // lets limit it to 0-1
 | 
					          max="1" // lets limit it to 0-1
 | 
				
			||||||
@@ -49,14 +69,35 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
          }}
 | 
					          }}
 | 
				
			||||||
        ></InputRange>
 | 
					        ></InputRange>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.TopP.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.TopP.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <InputRange
 | 
				
			||||||
 | 
					          aria={Locale.Settings.TopP.Title}
 | 
				
			||||||
 | 
					          value={(props.modelConfig.top_p ?? 1).toFixed(1)}
 | 
				
			||||||
 | 
					          min="0"
 | 
				
			||||||
 | 
					          max="1"
 | 
				
			||||||
 | 
					          step="0.1"
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) =>
 | 
				
			||||||
 | 
					                (config.top_p = ModalConfigValidator.top_p(
 | 
				
			||||||
 | 
					                  e.currentTarget.valueAsNumber,
 | 
				
			||||||
 | 
					                )),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        ></InputRange>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
      <ListItem
 | 
					      <ListItem
 | 
				
			||||||
        title={Locale.Settings.MaxTokens.Title}
 | 
					        title={Locale.Settings.MaxTokens.Title}
 | 
				
			||||||
        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
					        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.MaxTokens.Title}
 | 
				
			||||||
          type="number"
 | 
					          type="number"
 | 
				
			||||||
          min={100}
 | 
					          min={1024}
 | 
				
			||||||
          max={32000}
 | 
					          max={512000}
 | 
				
			||||||
          value={props.modelConfig.max_tokens}
 | 
					          value={props.modelConfig.max_tokens}
 | 
				
			||||||
          onChange={(e) =>
 | 
					          onChange={(e) =>
 | 
				
			||||||
            props.updateConfig(
 | 
					            props.updateConfig(
 | 
				
			||||||
@@ -68,11 +109,15 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        ></input>
 | 
					        ></input>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {props.modelConfig?.providerName == ServiceProvider.Google ? null : (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
          <ListItem
 | 
					          <ListItem
 | 
				
			||||||
        title={Locale.Settings.PresencePenlty.Title}
 | 
					            title={Locale.Settings.PresencePenalty.Title}
 | 
				
			||||||
        subTitle={Locale.Settings.PresencePenlty.SubTitle}
 | 
					            subTitle={Locale.Settings.PresencePenalty.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <InputRange
 | 
					            <InputRange
 | 
				
			||||||
 | 
					              aria={Locale.Settings.PresencePenalty.Title}
 | 
				
			||||||
              value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
					              value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
				
			||||||
              min="-2"
 | 
					              min="-2"
 | 
				
			||||||
              max="2"
 | 
					              max="2"
 | 
				
			||||||
@@ -89,15 +134,73 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
            ></InputRange>
 | 
					            ></InputRange>
 | 
				
			||||||
          </ListItem>
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.FrequencyPenalty.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <InputRange
 | 
				
			||||||
 | 
					              aria={Locale.Settings.FrequencyPenalty.Title}
 | 
				
			||||||
 | 
					              value={props.modelConfig.frequency_penalty?.toFixed(1)}
 | 
				
			||||||
 | 
					              min="-2"
 | 
				
			||||||
 | 
					              max="2"
 | 
				
			||||||
 | 
					              step="0.1"
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.frequency_penalty =
 | 
				
			||||||
 | 
					                      ModalConfigValidator.frequency_penalty(
 | 
				
			||||||
 | 
					                        e.currentTarget.valueAsNumber,
 | 
				
			||||||
 | 
					                      )),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></InputRange>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.InjectSystemPrompts.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Settings.InjectSystemPrompts.Title}
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={props.modelConfig.enableInjectSystemPrompts}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.enableInjectSystemPrompts =
 | 
				
			||||||
 | 
					                      e.currentTarget.checked),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.InputTemplate.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.InputTemplate.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Settings.InputTemplate.Title}
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              value={props.modelConfig.template}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) => (config.template = e.currentTarget.value),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      <ListItem
 | 
					      <ListItem
 | 
				
			||||||
        title={Locale.Settings.HistoryCount.Title}
 | 
					        title={Locale.Settings.HistoryCount.Title}
 | 
				
			||||||
        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
					        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InputRange
 | 
					        <InputRange
 | 
				
			||||||
 | 
					          aria={Locale.Settings.HistoryCount.Title}
 | 
				
			||||||
          title={props.modelConfig.historyMessageCount.toString()}
 | 
					          title={props.modelConfig.historyMessageCount.toString()}
 | 
				
			||||||
          value={props.modelConfig.historyMessageCount}
 | 
					          value={props.modelConfig.historyMessageCount}
 | 
				
			||||||
          min="0"
 | 
					          min="0"
 | 
				
			||||||
          max="32"
 | 
					          max="64"
 | 
				
			||||||
          step="1"
 | 
					          step="1"
 | 
				
			||||||
          onChange={(e) =>
 | 
					          onChange={(e) =>
 | 
				
			||||||
            props.updateConfig(
 | 
					            props.updateConfig(
 | 
				
			||||||
@@ -112,6 +215,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
					        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.CompressThreshold.Title}
 | 
				
			||||||
          type="number"
 | 
					          type="number"
 | 
				
			||||||
          min={500}
 | 
					          min={500}
 | 
				
			||||||
          max={4000}
 | 
					          max={4000}
 | 
				
			||||||
@@ -127,6 +231,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
					      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Memory.Title}
 | 
				
			||||||
          type="checkbox"
 | 
					          type="checkbox"
 | 
				
			||||||
          checked={props.modelConfig.sendMemory}
 | 
					          checked={props.modelConfig.sendMemory}
 | 
				
			||||||
          onChange={(e) =>
 | 
					          onChange={(e) =>
 | 
				
			||||||
@@ -136,6 +241,33 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        ></input>
 | 
					        ></input>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.CompressModel.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.CompressModel.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          className={styles["select-compress-model"]}
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.CompressModel.Title}
 | 
				
			||||||
 | 
					          value={compressModelValue}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            const [model, providerName] = getModelProvider(
 | 
				
			||||||
 | 
					              e.currentTarget.value,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            props.updateConfig((config) => {
 | 
				
			||||||
 | 
					              config.compressModel = ModalConfigValidator.model(model);
 | 
				
			||||||
 | 
					              config.compressProviderName = providerName as ServiceProvider;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {allModels
 | 
				
			||||||
 | 
					            .filter((v) => v.available)
 | 
				
			||||||
 | 
					            .map((v, i) => (
 | 
				
			||||||
 | 
					              <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
 | 
				
			||||||
 | 
					                {v.displayName}({v.provider?.providerName})
 | 
				
			||||||
 | 
					              </option>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					        </Select>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,31 +54,40 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  .actions {
 | 
					  .actions {
 | 
				
			||||||
    margin-top: 5vh;
 | 
					    margin-top: 5vh;
 | 
				
			||||||
    margin-bottom: 5vh;
 | 
					    margin-bottom: 2vh;
 | 
				
			||||||
    animation: slide-in ease 0.45s;
 | 
					    animation: slide-in ease 0.45s;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    .search-bar {
 | 
					 | 
				
			||||||
    font-size: 12px;
 | 
					    font-size: 12px;
 | 
				
			||||||
      margin-right: 10px;
 | 
					
 | 
				
			||||||
      width: 40vw;
 | 
					    .skip {
 | 
				
			||||||
 | 
					      margin-left: 10px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .masks {
 | 
					  .masks {
 | 
				
			||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: auto;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    padding-top: 20px;
 | 
					    padding-top: 20px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $linear: linear-gradient(
 | 
				
			||||||
 | 
					      to bottom,
 | 
				
			||||||
 | 
					      rgba(0, 0, 0, 0),
 | 
				
			||||||
 | 
					      rgba(0, 0, 0, 1),
 | 
				
			||||||
 | 
					      rgba(0, 0, 0, 0)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    -webkit-mask-image: $linear;
 | 
				
			||||||
 | 
					    mask-image: $linear;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    animation: slide-in ease 0.5s;
 | 
					    animation: slide-in ease 0.5s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .mask-row {
 | 
					    .mask-row {
 | 
				
			||||||
      margin-bottom: 10px;
 | 
					 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
      justify-content: center;
 | 
					      // justify-content: center;
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @for $i from 1 to 10 {
 | 
					      @for $i from 1 to 10 {
 | 
				
			||||||
        &:nth-child(#{$i * 2}) {
 | 
					        &:nth-child(#{$i * 2}) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,54 +5,29 @@ import { EmojiAvatar } from "./emoji";
 | 
				
			|||||||
import styles from "./new-chat.module.scss";
 | 
					import styles from "./new-chat.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import LeftIcon from "../icons/left.svg";
 | 
					import LeftIcon from "../icons/left.svg";
 | 
				
			||||||
import AddIcon from "../icons/lightning.svg";
 | 
					import LightningIcon from "../icons/lightning.svg";
 | 
				
			||||||
 | 
					import EyeIcon from "../icons/eye.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useLocation, useNavigate } from "react-router-dom";
 | 
					import { useLocation, useNavigate } from "react-router-dom";
 | 
				
			||||||
import { createEmptyMask, Mask, useMaskStore } from "../store/mask";
 | 
					import { Mask, useMaskStore } from "../store/mask";
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { useAppConfig, useChatStore } from "../store";
 | 
					import { useAppConfig, useChatStore } from "../store";
 | 
				
			||||||
import { MaskAvatar } from "./mask";
 | 
					import { MaskAvatar } from "./mask";
 | 
				
			||||||
 | 
					import { useCommand } from "../command";
 | 
				
			||||||
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
  const xmin = Math.max(aRect.x, bRect.x);
 | 
					import { BUILTIN_MASK_STORE } from "../masks";
 | 
				
			||||||
  const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
 | 
					import clsx from "clsx";
 | 
				
			||||||
  const ymin = Math.max(aRect.y, bRect.y);
 | 
					 | 
				
			||||||
  const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
 | 
					 | 
				
			||||||
  const width = xmax - xmin;
 | 
					 | 
				
			||||||
  const height = ymax - ymin;
 | 
					 | 
				
			||||||
  const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
 | 
					 | 
				
			||||||
  return intersectionArea;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
					function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
				
			||||||
  const domRef = useRef<HTMLDivElement>(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const changeOpacity = () => {
 | 
					 | 
				
			||||||
      const dom = domRef.current;
 | 
					 | 
				
			||||||
      const parent = document.getElementById(SlotID.AppBody);
 | 
					 | 
				
			||||||
      if (!parent || !dom) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const domRect = dom.getBoundingClientRect();
 | 
					 | 
				
			||||||
      const parentRect = parent.getBoundingClientRect();
 | 
					 | 
				
			||||||
      const intersectionArea = getIntersectionArea(domRect, parentRect);
 | 
					 | 
				
			||||||
      const domArea = domRect.width * domRect.height;
 | 
					 | 
				
			||||||
      const ratio = intersectionArea / domArea;
 | 
					 | 
				
			||||||
      const opacity = ratio > 0.9 ? 1 : 0.4;
 | 
					 | 
				
			||||||
      dom.style.opacity = opacity.toString();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setTimeout(changeOpacity, 30);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    window.addEventListener("resize", changeOpacity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return () => window.removeEventListener("resize", changeOpacity);
 | 
					 | 
				
			||||||
  }, [domRef]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
 | 
					    <div className={styles["mask"]} onClick={props.onClick}>
 | 
				
			||||||
      <MaskAvatar mask={props.mask} />
 | 
					      <MaskAvatar
 | 
				
			||||||
      <div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
 | 
					        avatar={props.mask.avatar}
 | 
				
			||||||
 | 
					        model={props.mask.modelConfig.model}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <div className={clsx(styles["mask-name"], "one-line")}>
 | 
				
			||||||
 | 
					        {props.mask.name}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -61,6 +36,7 @@ function useMaskGroup(masks: Mask[]) {
 | 
				
			|||||||
  const [groups, setGroups] = useState<Mask[][]>([]);
 | 
					  const [groups, setGroups] = useState<Mask[][]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const computeGroup = () => {
 | 
				
			||||||
      const appBody = document.getElementById(SlotID.AppBody);
 | 
					      const appBody = document.getElementById(SlotID.AppBody);
 | 
				
			||||||
      if (!appBody || masks.length === 0) return;
 | 
					      if (!appBody || masks.length === 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -86,7 +62,12 @@ function useMaskGroup(masks: Mask[]) {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      setGroups(newGroups);
 | 
					      setGroups(newGroups);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    computeGroup();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener("resize", computeGroup);
 | 
				
			||||||
 | 
					    return () => window.removeEventListener("resize", computeGroup);
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -103,13 +84,35 @@ export function NewChat() {
 | 
				
			|||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const maskRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { state } = useLocation();
 | 
					  const { state } = useLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const startChat = (mask?: Mask) => {
 | 
					  const startChat = (mask?: Mask) => {
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
      chatStore.newSession(mask);
 | 
					      chatStore.newSession(mask);
 | 
				
			||||||
      navigate(Path.Chat);
 | 
					      navigate(Path.Chat);
 | 
				
			||||||
 | 
					    }, 10);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useCommand({
 | 
				
			||||||
 | 
					    mask: (id) => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const mask = maskStore.get(id) ?? BUILTIN_MASK_STORE.get(id);
 | 
				
			||||||
 | 
					        startChat(mask ?? undefined);
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        console.error("[New Chat] failed to create chat from mask id=", id);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (maskRef.current) {
 | 
				
			||||||
 | 
					      maskRef.current.scrollLeft =
 | 
				
			||||||
 | 
					        (maskRef.current.scrollWidth - maskRef.current.clientWidth) / 2;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [groups]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["new-chat"]}>
 | 
					    <div className={styles["new-chat"]}>
 | 
				
			||||||
      <div className={styles["mask-header"]}>
 | 
					      <div className={styles["mask-header"]}>
 | 
				
			||||||
@@ -121,8 +124,8 @@ export function NewChat() {
 | 
				
			|||||||
        {!state?.fromHome && (
 | 
					        {!state?.fromHome && (
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
            text={Locale.NewChat.NotShow}
 | 
					            text={Locale.NewChat.NotShow}
 | 
				
			||||||
            onClick={() => {
 | 
					            onClick={async () => {
 | 
				
			||||||
              if (confirm(Locale.NewChat.ConfirmNoShow)) {
 | 
					              if (await showConfirm(Locale.NewChat.ConfirmNoShow)) {
 | 
				
			||||||
                startChat();
 | 
					                startChat();
 | 
				
			||||||
                config.update(
 | 
					                config.update(
 | 
				
			||||||
                  (config) => (config.dontShowMaskSplashScreen = true),
 | 
					                  (config) => (config.dontShowMaskSplashScreen = true),
 | 
				
			||||||
@@ -148,23 +151,25 @@ export function NewChat() {
 | 
				
			|||||||
      <div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
 | 
					      <div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className={styles["actions"]}>
 | 
					      <div className={styles["actions"]}>
 | 
				
			||||||
        <input
 | 
					        <IconButton
 | 
				
			||||||
          className={styles["search-bar"]}
 | 
					          text={Locale.NewChat.More}
 | 
				
			||||||
          placeholder={Locale.NewChat.More}
 | 
					 | 
				
			||||||
          type="text"
 | 
					 | 
				
			||||||
          onClick={() => navigate(Path.Masks)}
 | 
					          onClick={() => navigate(Path.Masks)}
 | 
				
			||||||
 | 
					          icon={<EyeIcon />}
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					          shadow
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <IconButton
 | 
					        <IconButton
 | 
				
			||||||
          text={Locale.NewChat.Skip}
 | 
					          text={Locale.NewChat.Skip}
 | 
				
			||||||
          onClick={() => startChat()}
 | 
					          onClick={() => startChat()}
 | 
				
			||||||
          icon={<AddIcon />}
 | 
					          icon={<LightningIcon />}
 | 
				
			||||||
          type="primary"
 | 
					          type="primary"
 | 
				
			||||||
          shadow
 | 
					          shadow
 | 
				
			||||||
 | 
					          className={styles["skip"]}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className={styles["masks"]}>
 | 
					      <div className={styles["masks"]} ref={maskRef}>
 | 
				
			||||||
        {groups.map((masks, i) => (
 | 
					        {groups.map((masks, i) => (
 | 
				
			||||||
          <div key={i} className={styles["mask-row"]}>
 | 
					          <div key={i} className={styles["mask-row"]}>
 | 
				
			||||||
            {masks.map((mask, index) => (
 | 
					            {masks.map((mask, index) => (
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								app/components/plugin.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/components/plugin.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					.plugin-title {
 | 
				
			||||||
 | 
					  font-weight: bolder;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  margin: 10px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.plugin-content {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  font-family: inherit;
 | 
				
			||||||
 | 
					  pre code {
 | 
				
			||||||
 | 
					    max-height: 240px;
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					    white-space: pre-wrap;
 | 
				
			||||||
 | 
					    min-width: 280px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plugin-schema {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					  flex-direction: row;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input {
 | 
				
			||||||
 | 
					    margin-right: 20px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					        margin-right: 0px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    button {
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user