mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-18 02:57:28 +00:00
Compare commits
1110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99a2fc5852 | ||
|
|
9d9070c899 | ||
|
|
9a48ed47f4 | ||
|
|
155f67e960 | ||
|
|
71778f4174 | ||
|
|
7bb66b8bec | ||
|
|
7bdec28e5f | ||
|
|
5ffdd9f542 | ||
|
|
4c72f2abed | ||
|
|
fd51f71e0f | ||
|
|
59f12d2582 | ||
|
|
f17a419520 | ||
|
|
ee114e14c3 | ||
|
|
78fb457765 | ||
|
|
8759ef012f | ||
|
|
f8d67a62a2 | ||
|
|
efb98854b2 | ||
|
|
7b29f429ee | ||
|
|
265c7d93a2 | ||
|
|
ce57ad3570 | ||
|
|
9282f1d893 | ||
|
|
9546a47f2b | ||
|
|
8073cbd96a | ||
|
|
5eba2f1d61 | ||
|
|
5ec421d8e6 | ||
|
|
1e25bf700d | ||
|
|
30fb349d91 | ||
|
|
d40fb68500 | ||
|
|
3049ad47e5 | ||
|
|
8945a3a2dd | ||
|
|
d191eef657 | ||
|
|
6ac7878863 | ||
|
|
c0a23ffa62 | ||
|
|
7d691f362d | ||
|
|
bf577b8937 | ||
|
|
819290c9b8 | ||
|
|
22e8b46159 | ||
|
|
76b8cc1168 | ||
|
|
fce07325b9 | ||
|
|
123862d41c | ||
|
|
7e298f8ad1 | ||
|
|
34aca14858 | ||
|
|
6b1f94348a | ||
|
|
4322037639 | ||
|
|
ae11f88595 | ||
|
|
389a4c3e4c | ||
|
|
efb691e6c2 | ||
|
|
53e3b35437 | ||
|
|
eb265a55e1 | ||
|
|
950f7d214f | ||
|
|
6bd2316d9c | ||
|
|
660180ea1b | ||
|
|
efc8457770 | ||
|
|
9b8b982d8a | ||
|
|
e6949e611a | ||
|
|
cffade7210 | ||
|
|
6b9237f868 | ||
|
|
1f4cf07b63 | ||
|
|
59a1f4c900 | ||
|
|
0a04a76c71 | ||
|
|
9e6bc518cc | ||
|
|
bfb6fbbac9 | ||
|
|
9c08d8cf20 | ||
|
|
281054ff4c | ||
|
|
3002659f47 | ||
|
|
647f8d7958 | ||
|
|
5d289d38ba | ||
|
|
05ea0dd54f | ||
|
|
1dad04ec09 | ||
|
|
2171117c53 | ||
|
|
d389befc9e | ||
|
|
3ced5ff144 | ||
|
|
38d3ab5acf | ||
|
|
ab32e15a86 | ||
|
|
25e17b95d5 | ||
|
|
d07224e658 | ||
|
|
aa15d45a3d | ||
|
|
c6c68da0b5 | ||
|
|
1a0aac81df | ||
|
|
39cb45c11c | ||
|
|
05d9aa53ef | ||
|
|
86f374df58 | ||
|
|
6935260bf0 | ||
|
|
f0d888729b | ||
|
|
6d7d4292ef | ||
|
|
fcefac9dbe | ||
|
|
ad5f731b20 | ||
|
|
76da067d40 | ||
|
|
0689670698 | ||
|
|
5a6f32c392 | ||
|
|
d6276c4692 | ||
|
|
29a44eb7ae | ||
|
|
048a625181 | ||
|
|
64782027c4 | ||
|
|
277645db50 | ||
|
|
3f53e4f53e | ||
|
|
0c5d4ca0a7 | ||
|
|
44495b153a | ||
|
|
de6e551cdb | ||
|
|
aeb393e391 | ||
|
|
db1b11deaf | ||
|
|
5a5e8ce652 | ||
|
|
6c31151430 | ||
|
|
a8ba2eba33 | ||
|
|
c974b1053c | ||
|
|
1ab75b8a92 | ||
|
|
75e3959474 | ||
|
|
bc371778b6 | ||
|
|
cd2870aebc | ||
|
|
7c72545217 | ||
|
|
2591ca3d60 | ||
|
|
c28190316f | ||
|
|
ffc22b8dac | ||
|
|
5367015a31 | ||
|
|
75c71c397e | ||
|
|
6192aebe66 | ||
|
|
a85a594597 | ||
|
|
014c9450ba | ||
|
|
63640f65e8 | ||
|
|
fd040988a3 | ||
|
|
f7c3b043b5 | ||
|
|
93e7675bc3 | ||
|
|
d7c97d4d34 | ||
|
|
dce794dbf7 | ||
|
|
093d86040f | ||
|
|
39617bc8c6 | ||
|
|
7da224ba92 | ||
|
|
df862732df | ||
|
|
fd4447f60a | ||
|
|
ea79d59aa0 | ||
|
|
41b0cf406c | ||
|
|
ef32cc8e0a | ||
|
|
ee8956b0e9 | ||
|
|
5ad9f8d931 | ||
|
|
ea379e1d0e | ||
|
|
b842baf21f | ||
|
|
58c9c7d5dd | ||
|
|
384fadf227 | ||
|
|
e4def0625b | ||
|
|
44d20de251 | ||
|
|
7ea33c2ddf | ||
|
|
b43423bffc | ||
|
|
cf4700a35c | ||
|
|
6bb552128c | ||
|
|
50b4fc06f8 | ||
|
|
f7f1be9df2 | ||
|
|
59574dc80f | ||
|
|
7577ec1ac4 | ||
|
|
d487be0029 | ||
|
|
83a3872b97 | ||
|
|
1ad2f63f85 | ||
|
|
fcaa8317e4 | ||
|
|
ccda14255a | ||
|
|
8d66828229 | ||
|
|
4ebf9e35e1 | ||
|
|
2902d6c7c2 | ||
|
|
01ef1fe4e4 | ||
|
|
c3d2d07b68 | ||
|
|
18417bacb3 | ||
|
|
8ec18dd21b | ||
|
|
edaff1c689 | ||
|
|
9c3a13cb23 | ||
|
|
0b326e7af4 | ||
|
|
1a1ff836b5 | ||
|
|
34fed74f64 | ||
|
|
f89b29928c | ||
|
|
2c6d4460c3 | ||
|
|
7afd3f97ee | ||
|
|
0708452939 | ||
|
|
a9e5d99ea3 | ||
|
|
a56d9ea98b | ||
|
|
f5e80af0b3 | ||
|
|
a1a7ddbc83 | ||
|
|
8b209d8926 | ||
|
|
9344cab59a | ||
|
|
03468e05e4 | ||
|
|
11792ba1a4 | ||
|
|
5baaa06896 | ||
|
|
d3286893c4 | ||
|
|
b087b20bac | ||
|
|
6a5a839d4d | ||
|
|
5d8a0952b4 | ||
|
|
bd08ecc1e0 | ||
|
|
e4f61c1084 | ||
|
|
a38215478f | ||
|
|
c192d07a04 | ||
|
|
098880b796 | ||
|
|
150c506ece | ||
|
|
f978d8224e | ||
|
|
ab59887933 | ||
|
|
458472f3e2 | ||
|
|
a9f98c5d39 | ||
|
|
2b7dff2d94 | ||
|
|
58752d2dcf | ||
|
|
67546f4b2a | ||
|
|
8e9dae7b5f | ||
|
|
fb4ff63bad | ||
|
|
1fed1ee567 | ||
|
|
02571c20ff | ||
|
|
8201daa4b4 | ||
|
|
5b54624cd5 | ||
|
|
db737567fb | ||
|
|
5c3898d13e | ||
|
|
2fdb2be6d0 | ||
|
|
ab78efc815 | ||
|
|
fcf97d1796 | ||
|
|
e85cc6acbe | ||
|
|
da002e6ca9 | ||
|
|
070e7b6911 | ||
|
|
d5a3eb7d04 | ||
|
|
616e6953cc | ||
|
|
b7c77777a5 | ||
|
|
8a79de333a | ||
|
|
a87d4271d3 | ||
|
|
7975cdf3bf | ||
|
|
b2badad554 | ||
|
|
133d8c9f77 | ||
|
|
9708d645d3 | ||
|
|
0bca4d3efc | ||
|
|
7572e791f6 | ||
|
|
16c63b3be9 | ||
|
|
37fbcb7950 | ||
|
|
a180d13182 | ||
|
|
a6363a502a | ||
|
|
81bc096872 | ||
|
|
edcdb378fd | ||
|
|
4447e51588 | ||
|
|
fb8aac650f | ||
|
|
ba6b0637cc | ||
|
|
3502730dfc | ||
|
|
b95c5bb8f4 | ||
|
|
f35784aa97 | ||
|
|
3746482e8c | ||
|
|
5ed4b60b8f | ||
|
|
547da2da60 | ||
|
|
f88ed4dd5c | ||
|
|
87fc681df3 | ||
|
|
a39b2f5aa7 | ||
|
|
0b9b21eafd | ||
|
|
21f43b0dd8 | ||
|
|
3a7ba5725c | ||
|
|
2e4fa32d63 | ||
|
|
0199896d9a | ||
|
|
edd9049100 | ||
|
|
290c763901 | ||
|
|
226446a3b5 | ||
|
|
ab627db4be | ||
|
|
0f35d2368f | ||
|
|
3c276d13c4 | ||
|
|
b7c3328d43 | ||
|
|
4d8e63bd1a | ||
|
|
51757b83e1 | ||
|
|
87c260093a | ||
|
|
691a878aa2 | ||
|
|
b33d808bc1 | ||
|
|
4559f5b2d3 | ||
|
|
0b9c6ecb00 | ||
|
|
a7d87475af | ||
|
|
ba37750943 | ||
|
|
4fc85d27e9 | ||
|
|
246ca40aac | ||
|
|
59a6fa7274 | ||
|
|
7fa21ce95f | ||
|
|
6b7295bbdf | ||
|
|
b4b6bd46fe | ||
|
|
d5c96cb036 | ||
|
|
1294d286ee | ||
|
|
dc95d0d1e6 | ||
|
|
467439090d | ||
|
|
b77574dad5 | ||
|
|
296da5dbcc | ||
|
|
3ac02879de | ||
|
|
a9160804a3 | ||
|
|
c48a398737 | ||
|
|
e735377218 | ||
|
|
d2b47969da | ||
|
|
af50660887 | ||
|
|
5adf1e272d | ||
|
|
abfb3f4006 | ||
|
|
5f05803643 | ||
|
|
ab0ba9f38c | ||
|
|
e1a93a1b82 | ||
|
|
e6e5f31921 | ||
|
|
8978dc7a8b | ||
|
|
d57e6425e5 | ||
|
|
b9b4b24961 | ||
|
|
4c05377c87 | ||
|
|
a9cdbce9de | ||
|
|
66403275b7 | ||
|
|
c554015526 | ||
|
|
35313ae0d6 | ||
|
|
6c359839cc | ||
|
|
be7e09b14d | ||
|
|
60b624a329 | ||
|
|
47531a6b93 | ||
|
|
0e05f725a4 | ||
|
|
034cc7f118 | ||
|
|
927cd07a3f | ||
|
|
070eba4b4c | ||
|
|
af9cc5ce11 | ||
|
|
f844772126 | ||
|
|
a8a2141626 | ||
|
|
0401f1e9ec | ||
|
|
358af20ad1 | ||
|
|
e455f06851 | ||
|
|
f191f981c4 | ||
|
|
9b659ed4f1 | ||
|
|
d39b52272e | ||
|
|
1ec2bbd533 | ||
|
|
a0ae6644ee | ||
|
|
1a7da8397b | ||
|
|
dcefd7dfb4 | ||
|
|
21edb75081 | ||
|
|
a28ab3628a | ||
|
|
856465ae59 | ||
|
|
3123d4bb9b | ||
|
|
dd21183261 | ||
|
|
d67d5d8006 | ||
|
|
c4f25a77d1 | ||
|
|
52763c09f2 | ||
|
|
ef4b0bc371 | ||
|
|
3d6859b865 | ||
|
|
0389e76af5 | ||
|
|
a1163dd735 | ||
|
|
a9a284a595 | ||
|
|
95bac28232 | ||
|
|
5bf5419633 | ||
|
|
48817648c3 | ||
|
|
4baaf456a7 | ||
|
|
52356a1b92 | ||
|
|
bdb7c9cbd7 | ||
|
|
a7b17eb1ba | ||
|
|
8ed68e4b12 | ||
|
|
f124404f07 | ||
|
|
3f89ee66e1 | ||
|
|
7c0302b5f8 | ||
|
|
26b70d6a25 | ||
|
|
2509f644bc | ||
|
|
896e1d978f | ||
|
|
6c4f64c397 | ||
|
|
d1f493bf17 | ||
|
|
56188c33b5 | ||
|
|
d9461a477d | ||
|
|
07b47fbf3a | ||
|
|
66d3206d7d | ||
|
|
136a46218b | ||
|
|
3f67db1028 | ||
|
|
936e593a4f | ||
|
|
9ff33405ec | ||
|
|
f25b084d40 | ||
|
|
fe00434454 | ||
|
|
f2957ee558 | ||
|
|
b605ff9b02 | ||
|
|
b035b4d8af | ||
|
|
5d3a6caae5 | ||
|
|
7daf1f63e6 | ||
|
|
bed19d5ca4 | ||
|
|
96183e6664 | ||
|
|
d99cafbb09 | ||
|
|
4759cda8f7 | ||
|
|
ce8858716a | ||
|
|
ecb0553c6d | ||
|
|
e4217f64d3 | ||
|
|
cbb6bcc4ac | ||
|
|
845b748ffe | ||
|
|
b3209030b0 | ||
|
|
410b8afe6d | ||
|
|
cf967d39ea | ||
|
|
f2f3bad9ef | ||
|
|
5f95b4a0b7 | ||
|
|
340f86f3cc | ||
|
|
768ab854d6 | ||
|
|
452f648d75 | ||
|
|
dc0f303bb7 | ||
|
|
27bbd951f0 | ||
|
|
7d8a47123d | ||
|
|
c95fb55c51 | ||
|
|
a80bc02b96 | ||
|
|
17e1ea5f4b | ||
|
|
587f420344 | ||
|
|
9dbfd1b0af | ||
|
|
74be7b20f6 | ||
|
|
ef5832777d | ||
|
|
8184357b49 | ||
|
|
7a83060012 | ||
|
|
d05adbbb9b | ||
|
|
5f79709b4e | ||
|
|
86354e305e | ||
|
|
4eef3feef3 | ||
|
|
865377449e | ||
|
|
a4fabbe299 | ||
|
|
f67843b963 | ||
|
|
bf296d92a5 | ||
|
|
253b8cc899 | ||
|
|
1a6f332223 | ||
|
|
1b78a33aac | ||
|
|
3bd98f62f7 | ||
|
|
a6d315e14c | ||
|
|
f343d9ca2b | ||
|
|
b5708ec51c | ||
|
|
b47274bfad | ||
|
|
97a8219845 | ||
|
|
c26599ef46 | ||
|
|
a92952f070 | ||
|
|
77d5dff0c6 | ||
|
|
02e43ee12e | ||
|
|
7bced6b236 | ||
|
|
a0844d5481 | ||
|
|
d79b9e266e | ||
|
|
6acfe31ee9 | ||
|
|
2c95a7c277 | ||
|
|
7010450f77 | ||
|
|
c9849ecc46 | ||
|
|
5b641a4ead | ||
|
|
b73af9e88f | ||
|
|
ed84f937e3 | ||
|
|
6bf8a72011 | ||
|
|
d3b93196cf | ||
|
|
4989892830 | ||
|
|
b7c742166a | ||
|
|
fcc4d0074f | ||
|
|
cb83a06103 | ||
|
|
5018945c71 | ||
|
|
ce2fba7f8b | ||
|
|
2b898bc577 | ||
|
|
017fa70e1a | ||
|
|
5f52148e4e | ||
|
|
7e9bd35ac7 | ||
|
|
d124ec5b1a | ||
|
|
8202c115f0 | ||
|
|
b778cd2b23 | ||
|
|
6e7249cf06 | ||
|
|
33014e9399 | ||
|
|
387721e907 | ||
|
|
e0cc13094f | ||
|
|
5dc3543e41 | ||
|
|
f1f07cb31b | ||
|
|
a26dbf5358 | ||
|
|
49e77fb3df | ||
|
|
e8f78c739c | ||
|
|
4c1f341226 | ||
|
|
3721ac1522 | ||
|
|
4d18b263dd | ||
|
|
9496dac448 | ||
|
|
0d724af6e3 | ||
|
|
4da5e74d23 | ||
|
|
ac9f632aff | ||
|
|
83d58848bc | ||
|
|
2100d32bab | ||
|
|
b4be218af8 | ||
|
|
0f94b69c38 | ||
|
|
8fb549bd76 | ||
|
|
bc977909cc | ||
|
|
1c3cd7984c | ||
|
|
12f104337b | ||
|
|
4b3791e6dc | ||
|
|
f17b4f0760 | ||
|
|
a13f80f15b | ||
|
|
f81225788d | ||
|
|
b0a5d01e1d | ||
|
|
c2f209abb7 | ||
|
|
3217f0df3e | ||
|
|
2257714fdc | ||
|
|
a52a67176e | ||
|
|
f13e4bf486 | ||
|
|
cbda794143 | ||
|
|
5ef5664241 | ||
|
|
eff9ce117f | ||
|
|
191f521926 | ||
|
|
e77555a04f | ||
|
|
8e6039b995 | ||
|
|
4a313a5f93 | ||
|
|
3665ad672e | ||
|
|
eb812451f2 | ||
|
|
a8f4ae2a73 | ||
|
|
ac83041a44 | ||
|
|
01c84f9a45 | ||
|
|
a41a7e6511 | ||
|
|
0af047b18c | ||
|
|
49fc6ab474 | ||
|
|
a04674c72f | ||
|
|
4e20a747bc | ||
|
|
a4942062de | ||
|
|
c7e812361d | ||
|
|
c71255461d | ||
|
|
b366bf585c | ||
|
|
494c386ca8 | ||
|
|
7362047e51 | ||
|
|
f9ddec3b1c | ||
|
|
d39c9cbec6 | ||
|
|
9693df9bf3 | ||
|
|
5fa9966a4e | ||
|
|
ce88b75f61 | ||
|
|
50d40f04ec | ||
|
|
933327baf7 | ||
|
|
7f3649996c | ||
|
|
2ff79fcf65 | ||
|
|
6704491906 | ||
|
|
4ccd36ea19 | ||
|
|
2eb1f65d3f | ||
|
|
e11a8514b1 | ||
|
|
943f21f3cb | ||
|
|
3f45153e75 | ||
|
|
d27981bd34 | ||
|
|
5a22f33bcf | ||
|
|
28d90f6754 | ||
|
|
a92373c78c | ||
|
|
b0a145fd5b | ||
|
|
64b565dc15 | ||
|
|
a7535aab99 | ||
|
|
8a65a4174a | ||
|
|
e548e411bd | ||
|
|
b0cbf71a1c | ||
|
|
3269926283 | ||
|
|
5894e18f4f | ||
|
|
2250f35a7e | ||
|
|
fe7cd5aa8d | ||
|
|
d459b03e84 | ||
|
|
e5d0f26fb9 | ||
|
|
e39391cfb0 | ||
|
|
f422a0588b | ||
|
|
2bfba7a479 | ||
|
|
0bafdf3381 | ||
|
|
40e640511b | ||
|
|
5930bb88bf | ||
|
|
8948e99eeb | ||
|
|
37caafc722 | ||
|
|
18c2e5cd98 | ||
|
|
f9c8a802ef | ||
|
|
07ffc36678 | ||
|
|
3a5013b876 | ||
|
|
bafb0078e2 | ||
|
|
148c974912 | ||
|
|
d534d4575d | ||
|
|
1d37867f39 | ||
|
|
70b673d12c | ||
|
|
e78523034a | ||
|
|
7874d27486 | ||
|
|
cc3f3cf033 | ||
|
|
90d4e0e41c | ||
|
|
7783fe802a | ||
|
|
2cc9e62852 | ||
|
|
26ef7aae38 | ||
|
|
efe4ea0e25 | ||
|
|
9fb9dfb905 | ||
|
|
aa49d2a360 | ||
|
|
5107f1b84a | ||
|
|
ffdedde6ac | ||
|
|
ee698ab5be | ||
|
|
f1ee9a301d | ||
|
|
611d77e1a9 | ||
|
|
418a7518d8 | ||
|
|
b05bb899f1 | ||
|
|
c51a30b862 | ||
|
|
533f9a0d84 | ||
|
|
9c4d3a6359 | ||
|
|
6936a795a6 | ||
|
|
74defce481 | ||
|
|
1c4d7fd84b | ||
|
|
2bc07c6b23 | ||
|
|
1a11e33749 | ||
|
|
135a93993b | ||
|
|
d1b192cd72 | ||
|
|
efed150910 | ||
|
|
6242cc31f2 | ||
|
|
71df716787 | ||
|
|
78353cb538 | ||
|
|
caff73a746 | ||
|
|
02bc3cde53 | ||
|
|
f7a16c6ca5 | ||
|
|
b548c6c827 | ||
|
|
4ae8bf2f71 | ||
|
|
0a848c2d6c | ||
|
|
eeb9fe9b7f | ||
|
|
fbb189ecd7 | ||
|
|
2abf2c464f | ||
|
|
9c5ab755c1 | ||
|
|
c5ed0753a6 | ||
|
|
faa7abcc7f | ||
|
|
3d9587f128 | ||
|
|
bbf7fe2d1d | ||
|
|
96f338c964 | ||
|
|
21d68f61ea | ||
|
|
f47bc44dbc | ||
|
|
f907c25b21 | ||
|
|
1b64db5521 | ||
|
|
75c94d9374 | ||
|
|
31ece25252 | ||
|
|
d608a6f123 | ||
|
|
2dcd6fa2b9 | ||
|
|
19cd98cb99 | ||
|
|
5a370f17f2 | ||
|
|
cd5960686f | ||
|
|
c1a70ad690 | ||
|
|
66778efcc5 | ||
|
|
361b0abec9 | ||
|
|
e01b517843 | ||
|
|
f613a79f3e | ||
|
|
6bb49ade76 | ||
|
|
87540b4f7c | ||
|
|
e3d7b31a49 | ||
|
|
bf016543c3 | ||
|
|
eb94aa13e6 | ||
|
|
16b9cb6ff4 | ||
|
|
6e72dcd0ba | ||
|
|
96ab4177ca | ||
|
|
76824a0337 | ||
|
|
22af6af9c7 | ||
|
|
d542b529cb | ||
|
|
a7c79a9e34 | ||
|
|
e85f687c6b | ||
|
|
acdfd86286 | ||
|
|
1e57317322 | ||
|
|
16ad2d48d8 | ||
|
|
21077d4696 | ||
|
|
d3a6f1cc46 | ||
|
|
5b5f10cadc | ||
|
|
fa06ea19a6 | ||
|
|
3454d6c29e | ||
|
|
d96eb6fb1c | ||
|
|
693dfd18f9 | ||
|
|
3cd29a4963 | ||
|
|
41120b4d75 | ||
|
|
30d5a11f46 | ||
|
|
368fd75c86 | ||
|
|
ee07762611 | ||
|
|
a215538b4d | ||
|
|
873e3f3dc8 | ||
|
|
0298692852 | ||
|
|
91ff211ab1 | ||
|
|
156ad5c3fd | ||
|
|
d90e4bef63 | ||
|
|
39329fcd1c | ||
|
|
738a9a4558 | ||
|
|
96709dd9f3 | ||
|
|
072ac1b3c8 | ||
|
|
46a67e09f1 | ||
|
|
59de8e11ac | ||
|
|
dc5e53ec14 | ||
|
|
00c1ff05de | ||
|
|
33ae3479c4 | ||
|
|
18344ae580 | ||
|
|
de98d11d65 | ||
|
|
dadc2cf329 | ||
|
|
452853c1a4 | ||
|
|
c6ead4e5bd | ||
|
|
a044781423 | ||
|
|
b564cac048 | ||
|
|
fbdad581b5 | ||
|
|
e9af621d88 | ||
|
|
0595636ceb | ||
|
|
eadf9aad41 | ||
|
|
d95c2436d7 | ||
|
|
2cc2d4f652 | ||
|
|
eb69ada880 | ||
|
|
1660c47db5 | ||
|
|
eba661ad1e | ||
|
|
6d11fbee89 | ||
|
|
9a6c540013 | ||
|
|
6be78ff283 | ||
|
|
1644b7b15d | ||
|
|
0befa28e8e | ||
|
|
ce91049827 | ||
|
|
e911eb7988 | ||
|
|
66a8612d12 | ||
|
|
f796c3b216 | ||
|
|
c53a48cde5 | ||
|
|
d95583ce1d | ||
|
|
67a65213d8 | ||
|
|
0f3216564d | ||
|
|
9a59da16a5 | ||
|
|
e18001299b | ||
|
|
66bdfe180c | ||
|
|
5281f2ba64 | ||
|
|
e1190f98e9 | ||
|
|
e07163c568 | ||
|
|
a5bccd02dc | ||
|
|
1f9fc09989 | ||
|
|
64973e6cff | ||
|
|
c6d7cc7c25 | ||
|
|
0118364059 | ||
|
|
28d401ec01 | ||
|
|
881ad57a02 | ||
|
|
c75421e2c6 | ||
|
|
23cf1d268c | ||
|
|
cb281dfc11 | ||
|
|
4afe7a29b1 | ||
|
|
a726818c17 | ||
|
|
26c3da3548 | ||
|
|
4640d0a4aa | ||
|
|
bcd673de3a | ||
|
|
55a49baed7 | ||
|
|
bcbb9bb16a | ||
|
|
305d7528da | ||
|
|
1b0d7fbd56 | ||
|
|
85c40424d5 | ||
|
|
c04a816e59 | ||
|
|
498d73f67c | ||
|
|
69420f713f | ||
|
|
9c12e02cb5 | ||
|
|
59b1e970fd | ||
|
|
7739219ca6 | ||
|
|
1242f35177 | ||
|
|
9247661849 | ||
|
|
16570909be | ||
|
|
1ea0dd8f06 | ||
|
|
0ca17d3e6d | ||
|
|
a391ac29a0 | ||
|
|
9927e5d191 | ||
|
|
7171a69512 | ||
|
|
e379ee8f66 | ||
|
|
59aabb4311 | ||
|
|
4825404d37 | ||
|
|
ea04e6bcc5 | ||
|
|
108b67be6c | ||
|
|
29c95c598e | ||
|
|
b2499b0a7e | ||
|
|
12737fb7e5 | ||
|
|
f17f38e569 | ||
|
|
b2cad22952 | ||
|
|
e763124b69 | ||
|
|
153012789d | ||
|
|
d985563516 | ||
|
|
58dc7ad770 | ||
|
|
28cdfc0a14 | ||
|
|
7b176015b8 | ||
|
|
cc2d9f539d | ||
|
|
7f86bdf548 | ||
|
|
0d929800cf | ||
|
|
9ebfcaf6aa | ||
|
|
40efa73a42 | ||
|
|
4a59b3ccd6 | ||
|
|
ec61534256 | ||
|
|
2a218c1c89 | ||
|
|
993cd6b624 | ||
|
|
3d4bd76083 | ||
|
|
7192437863 | ||
|
|
4bbcb00d13 | ||
|
|
9de24668d8 | ||
|
|
7aa54a2cd7 | ||
|
|
a836e97315 | ||
|
|
3373f5e0a0 | ||
|
|
d6e601b424 | ||
|
|
8c3a559690 | ||
|
|
c008d391df | ||
|
|
7c29844e4a | ||
|
|
90d85a6f0a | ||
|
|
d40429ad93 | ||
|
|
30806ef270 | ||
|
|
02acc52fdb | ||
|
|
3458476115 | ||
|
|
61c685ad79 | ||
|
|
0121795a84 | ||
|
|
ae254f5368 | ||
|
|
562448b441 | ||
|
|
04f7d89399 | ||
|
|
0d456df588 | ||
|
|
dc3b453b05 | ||
|
|
b19e1b8207 | ||
|
|
97b5ca8099 | ||
|
|
4ecf5dde14 | ||
|
|
65ccfd0848 | ||
|
|
2621b77f9a | ||
|
|
65a15dbc17 | ||
|
|
c0095d4521 | ||
|
|
5043075135 | ||
|
|
10ef61eedb | ||
|
|
dc9e3b4139 | ||
|
|
27e3aa828c | ||
|
|
d859e3fa64 | ||
|
|
459c277c94 | ||
|
|
5639f1c2d8 | ||
|
|
0cf4c59d22 | ||
|
|
350c29a054 | ||
|
|
1fa4ac69b2 | ||
|
|
19d1f7853f | ||
|
|
74572ab2ee | ||
|
|
3d243c3ee2 | ||
|
|
87188cd7d4 | ||
|
|
bbab729619 | ||
|
|
1c67dd3c31 | ||
|
|
0be3678c9c | ||
|
|
1cb4d750e4 | ||
|
|
88ed83f419 | ||
|
|
1513ed7847 | ||
|
|
1e1d24d1b0 | ||
|
|
b7fd1e4a20 | ||
|
|
7e7d6112ca | ||
|
|
6c3fb7777e | ||
|
|
18b3300ff1 | ||
|
|
bae57c05c1 | ||
|
|
3def2bbd30 | ||
|
|
419a056fbf | ||
|
|
48af027903 | ||
|
|
9bf90c3baf | ||
|
|
fe3232bf23 | ||
|
|
1236fa8fe4 | ||
|
|
e097d5a538 | ||
|
|
425feb88d8 | ||
|
|
bc322ddac4 | ||
|
|
fd6838e690 | ||
|
|
b64480b750 | ||
|
|
da6423de33 | ||
|
|
efc9d200b1 | ||
|
|
fe37718259 | ||
|
|
c412fd9cde | ||
|
|
54f5b1a951 | ||
|
|
a9b9d23586 | ||
|
|
168226ba10 | ||
|
|
1a8fd61a98 | ||
|
|
2bd2d73d33 | ||
|
|
62da481dc6 | ||
|
|
4217358de7 | ||
|
|
bb9f5a4a6d | ||
|
|
935acccca4 | ||
|
|
453a42fad9 | ||
|
|
58101328c5 | ||
|
|
a03c615fa4 | ||
|
|
487ef35c58 | ||
|
|
f9f32a0158 | ||
|
|
ea10806cf9 | ||
|
|
1a9ebb54b2 | ||
|
|
6de3857150 | ||
|
|
32cd890b6e | ||
|
|
f968d77365 | ||
|
|
dc22f7d32f | ||
|
|
c2b33e3b23 | ||
|
|
db3326deae | ||
|
|
25ae077ac9 | ||
|
|
aaa41a8074 | ||
|
|
26f5b954c5 | ||
|
|
79c6dd08c9 | ||
|
|
17e8a3432a | ||
|
|
790af65b2c | ||
|
|
6522147183 | ||
|
|
0755ac9991 | ||
|
|
4c4dc6e8b4 | ||
|
|
1eebdc4773 | ||
|
|
9b6c898675 | ||
|
|
ee4f27d01b | ||
|
|
995c19a997 | ||
|
|
e385e347ea | ||
|
|
71d0d759da | ||
|
|
eb75ff232f | ||
|
|
272662089d | ||
|
|
214ca4db56 | ||
|
|
473e8e0eaf | ||
|
|
99efc1fbb6 | ||
|
|
d283f6b35f | ||
|
|
2f3acd9d22 | ||
|
|
eee6dee599 | ||
|
|
dcf7878772 | ||
|
|
97bc2b4474 | ||
|
|
ef8ae4db80 | ||
|
|
90576d0261 | ||
|
|
4b3e30e669 | ||
|
|
75570af967 | ||
|
|
cca9c0479f | ||
|
|
8a2332074f | ||
|
|
2ec4565601 | ||
|
|
a4fb33957f | ||
|
|
909c5eb276 | ||
|
|
8723e3f239 | ||
|
|
9328b907f2 | ||
|
|
8efa12b941 | ||
|
|
7b997b3a2c | ||
|
|
700c05b826 | ||
|
|
c5103237b0 | ||
|
|
f500eb17a8 | ||
|
|
86f6bb7abe | ||
|
|
c4c1099ae5 | ||
|
|
c869455456 | ||
|
|
f89d8a0fe5 | ||
|
|
3d6d19903b | ||
|
|
c5f1a0c712 | ||
|
|
524d4a65bf | ||
|
|
082218173a | ||
|
|
67cbbc2266 | ||
|
|
79b35e385f | ||
|
|
03e8ab4126 | ||
|
|
30f32c6a6d | ||
|
|
5813ca780f | ||
|
|
aa34c3035a | ||
|
|
fb9f595044 | ||
|
|
f24de65626 | ||
|
|
e34dccbc65 | ||
|
|
f6e8887482 | ||
|
|
a29f4d88c5 | ||
|
|
a6bb30af41 | ||
|
|
09adc6f201 | ||
|
|
6b79b89dc0 | ||
|
|
424424c160 | ||
|
|
e5baa6ee1c | ||
|
|
9207d729ca | ||
|
|
27933da884 | ||
|
|
454dac17ea | ||
|
|
1921ac3692 | ||
|
|
42a2418d9a | ||
|
|
5cb317bdbd | ||
|
|
37dd1ef099 | ||
|
|
5fa6462412 | ||
|
|
a882e680ae | ||
|
|
552e2850c5 | ||
|
|
c418d9ed9a | ||
|
|
1dc2284d57 | ||
|
|
f4cc90c8d6 | ||
|
|
140d3a974b | ||
|
|
2ecb742e47 | ||
|
|
9066cfa8a0 | ||
|
|
4f437f30e0 | ||
|
|
3c2a86f94d | ||
|
|
1b07282153 | ||
|
|
af7f886c39 | ||
|
|
9cfa138796 | ||
|
|
dc132655a6 | ||
|
|
a378665b8c | ||
|
|
3516aad349 | ||
|
|
58525c574b | ||
|
|
1df39e5a7f | ||
|
|
be6ffd3c60 | ||
|
|
a9522075c6 | ||
|
|
983d31bfd3 | ||
|
|
20c043f584 | ||
|
|
73263e02d6 | ||
|
|
7143b0f160 | ||
|
|
dd82618c05 | ||
|
|
19935ee8ac | ||
|
|
6fef5aaf22 | ||
|
|
b5aa3c129b | ||
|
|
8c7c39550c | ||
|
|
962e803d8a | ||
|
|
ff57ced2bb | ||
|
|
2223806c00 | ||
|
|
d1c62a583d | ||
|
|
53b3599827 | ||
|
|
b3b1c803fc | ||
|
|
a4a40c495d | ||
|
|
ee302c063c | ||
|
|
5a67bdf1b0 | ||
|
|
2c81a5f0cc | ||
|
|
b84b6affe9 | ||
|
|
c183c1231c | ||
|
|
54e738941d | ||
|
|
dd393cd0d9 | ||
|
|
e98849048c | ||
|
|
8e68bcce29 | ||
|
|
892d014c26 | ||
|
|
19bfa158cc | ||
|
|
69e44a03b1 | ||
|
|
9a78db8484 | ||
|
|
a381163402 | ||
|
|
1644dbc864 | ||
|
|
cc1400e939 | ||
|
|
6187656aa9 | ||
|
|
e5b6aa6e85 | ||
|
|
7e46d4217d | ||
|
|
23596d22c9 | ||
|
|
c25d4d8d23 | ||
|
|
b291fbff6b | ||
|
|
e68edf81f7 | ||
|
|
5ff16f9b2d | ||
|
|
f614cfa563 | ||
|
|
2048b451bf | ||
|
|
bd48f43410 | ||
|
|
c47d8a10f0 | ||
|
|
c0b9350785 | ||
|
|
229738cda9 | ||
|
|
39d95172e8 | ||
|
|
5059cbdb46 | ||
|
|
a981e10712 | ||
|
|
f7852ada97 | ||
|
|
495bbcb621 | ||
|
|
20e34bec7e | ||
|
|
0033f5ba2e | ||
|
|
e52ac52e7b | ||
|
|
66682584a5 | ||
|
|
1a2bf8df1f | ||
|
|
1819c4d5f5 | ||
|
|
6f24dddcb2 | ||
|
|
8de29fbb83 | ||
|
|
f2163acf2b | ||
|
|
5259acfacd | ||
|
|
c433af284c | ||
|
|
3122b8a36a | ||
|
|
bbe7223a85 | ||
|
|
2af05c166c | ||
|
|
ecb5b5630c | ||
|
|
e1b9f164f9 | ||
|
|
3223c7e181 | ||
|
|
ccfac06645 | ||
|
|
69db1f1465 | ||
|
|
94549f9687 | ||
|
|
c7e1bab18a | ||
|
|
627f95b034 | ||
|
|
8b99eec440 | ||
|
|
49bfd2b719 | ||
|
|
434e9d7695 | ||
|
|
b2938ffe2c | ||
|
|
d9cf0885f1 | ||
|
|
3ed50787b3 | ||
|
|
97d948cdb1 | ||
|
|
5017fabbfa | ||
|
|
bd5c261b99 | ||
|
|
00c2d6c102 | ||
|
|
4a8bb625b8 | ||
|
|
db01994cd0 | ||
|
|
a0ca3effa7 | ||
|
|
5a10ebd384 | ||
|
|
68097c132d | ||
|
|
3352bacd35 | ||
|
|
7fcb14e25f | ||
|
|
867187ab4d | ||
|
|
3ad96d3b4e | ||
|
|
d9390ff4c3 | ||
|
|
8c209e2fb9 | ||
|
|
a9bfcb0daf | ||
|
|
bb848b2fe0 | ||
|
|
618908f6f8 | ||
|
|
1f4ebddcfa | ||
|
|
6d79d8993e | ||
|
|
7c03ad71de | ||
|
|
4f194f4e6a | ||
|
|
81137e0533 | ||
|
|
b9b66dda54 | ||
|
|
fd22948ead | ||
|
|
894dce7366 | ||
|
|
b95142bbac | ||
|
|
7f74a9664e | ||
|
|
a3739f67f7 | ||
|
|
b841ce006f | ||
|
|
e3f9ef1894 | ||
|
|
558e625a01 | ||
|
|
37a83ecc33 | ||
|
|
37bb34b4b0 | ||
|
|
8deab221f9 | ||
|
|
17e9f1a07d | ||
|
|
792754cee3 | ||
|
|
98b27a17a6 | ||
|
|
7855f83e2d | ||
|
|
cbdf26bf2c | ||
|
|
eb46b71a71 | ||
|
|
a42c3b6227 | ||
|
|
b00dd8b405 | ||
|
|
be228ccd2c | ||
|
|
b1be64bcf3 | ||
|
|
6ecfb81cbc | ||
|
|
14848ff789 | ||
|
|
47d3b515da | ||
|
|
760514c3e1 | ||
|
|
254c25c27a | ||
|
|
8731a32e56 | ||
|
|
7208a65e5d | ||
|
|
4084b18071 | ||
|
|
2ca0d7246d | ||
|
|
d042a1bd55 | ||
|
|
816e831a2e | ||
|
|
a3ceae4a86 | ||
|
|
eb163d9c94 | ||
|
|
a592a81bc2 | ||
|
|
bb300d199e | ||
|
|
7dbb6b017c | ||
|
|
ce1854847b | ||
|
|
2f9faba40d | ||
|
|
a5085014cc | ||
|
|
18d3706ff8 | ||
|
|
152950497e | ||
|
|
d6fd50e382 | ||
|
|
cfd3f6c073 | ||
|
|
45c56b5ded | ||
|
|
d306394f33 | ||
|
|
cdba87a7da | ||
|
|
ae5b874a6c | ||
|
|
d0bc8d17d1 | ||
|
|
4784ca7514 | ||
|
|
3a18c0ce9f | ||
|
|
929668bead | ||
|
|
06a78f9042 | ||
|
|
0f1c4c4ebe | ||
|
|
1bcf7a3c39 | ||
|
|
5f0b3f6d6f | ||
|
|
19a318c943 | ||
|
|
13ab0f8e4f | ||
|
|
6d8d40e67b | ||
|
|
287caf8e38 | ||
|
|
c802b3b41a | ||
|
|
ed4e1c2332 | ||
|
|
e581ea33c2 | ||
|
|
bf80d71ddf | ||
|
|
e19b244e73 | ||
|
|
f451268830 | ||
|
|
069f2672c1 | ||
|
|
ccf13d445f | ||
|
|
da4d1861fe | ||
|
|
3de5b96cb4 | ||
|
|
5b9e275690 | ||
|
|
607e3206b3 | ||
|
|
83feb492fb | ||
|
|
4f212be45c | ||
|
|
92918e3751 | ||
|
|
de15551570 | ||
|
|
a81a28b7a5 | ||
|
|
dc36fdedc2 | ||
|
|
3017882fa3 | ||
|
|
e9ba392af8 | ||
|
|
83a37e4653 | ||
|
|
b6f95dca41 | ||
|
|
7ff4cebdbe | ||
|
|
af00f7b311 | ||
|
|
cc1d6e1c05 | ||
|
|
6c7a8c811c |
27
.env.example
27
.env.example
@@ -7,6 +7,8 @@
|
|||||||
# 调试相关配置
|
# 调试相关配置
|
||||||
# 启用pprof
|
# 启用pprof
|
||||||
# ENABLE_PPROF=true
|
# ENABLE_PPROF=true
|
||||||
|
# 启用调试模式
|
||||||
|
# DEBUG=true
|
||||||
|
|
||||||
# 数据库相关配置
|
# 数据库相关配置
|
||||||
# 数据库连接字符串
|
# 数据库连接字符串
|
||||||
@@ -41,6 +43,14 @@
|
|||||||
# 更新任务启用
|
# 更新任务启用
|
||||||
# UPDATE_TASK=true
|
# UPDATE_TASK=true
|
||||||
|
|
||||||
|
# 对话超时设置
|
||||||
|
# 所有请求超时时间,单位秒,默认为0,表示不限制
|
||||||
|
# RELAY_TIMEOUT=0
|
||||||
|
# 流模式无响应超时时间,单位秒,如果出现空补全可以尝试改为更大值
|
||||||
|
# STREAMING_TIMEOUT=120
|
||||||
|
|
||||||
|
# Gemini 识别图片 最大图片数量
|
||||||
|
# GEMINI_VISION_MAX_IMAGE_NUM=16
|
||||||
|
|
||||||
# 会话密钥
|
# 会话密钥
|
||||||
# SESSION_SECRET=random_string
|
# SESSION_SECRET=random_string
|
||||||
@@ -50,10 +60,6 @@
|
|||||||
# CHANNEL_TEST_FREQUENCY=10
|
# CHANNEL_TEST_FREQUENCY=10
|
||||||
# 生成默认token
|
# 生成默认token
|
||||||
# GENERATE_DEFAULT_TOKEN=false
|
# GENERATE_DEFAULT_TOKEN=false
|
||||||
# Gemini 安全设置
|
|
||||||
# GEMINI_SAFETY_SETTING=BLOCK_NONE
|
|
||||||
# Gemini版本设置
|
|
||||||
# GEMINI_MODEL_MAP=gemini-1.0-pro:v1
|
|
||||||
# Cohere 安全设置
|
# Cohere 安全设置
|
||||||
# COHERE_SAFETY_SETTING=NONE
|
# COHERE_SAFETY_SETTING=NONE
|
||||||
# 是否统计图片token
|
# 是否统计图片token
|
||||||
@@ -62,10 +68,19 @@
|
|||||||
# GET_MEDIA_TOKEN_NOT_STREAM=true
|
# GET_MEDIA_TOKEN_NOT_STREAM=true
|
||||||
# 设置 Dify 渠道是否输出工作流和节点信息到客户端
|
# 设置 Dify 渠道是否输出工作流和节点信息到客户端
|
||||||
# DIFY_DEBUG=true
|
# DIFY_DEBUG=true
|
||||||
# 设置流式一次回复的超时时间
|
|
||||||
# STREAMING_TIMEOUT=90
|
|
||||||
|
|
||||||
|
|
||||||
# 节点类型
|
# 节点类型
|
||||||
# 如果是主节点则为master
|
# 如果是主节点则为master
|
||||||
# NODE_TYPE=master
|
# NODE_TYPE=master
|
||||||
|
|
||||||
|
|
||||||
|
# JavaScript 运行时配置
|
||||||
|
# 是否启用(默认:false)
|
||||||
|
# JS_RUNTIME_ENABLED=true
|
||||||
|
# 最大虚拟机数量(默认:8)
|
||||||
|
# JS_MAX_VM_COUNT=
|
||||||
|
# 运行超时时间(单位:秒,默认:5)
|
||||||
|
# JS_SCRIPT_TIMEOUT=
|
||||||
|
# 脚本文件夹(默认:scripts/)
|
||||||
|
# JS_SCRIPT_PATH=
|
||||||
|
|||||||
19
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal file
19
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
### PR 类型
|
||||||
|
|
||||||
|
- [ ] Bug 修复
|
||||||
|
- [ ] 新功能
|
||||||
|
- [ ] 文档更新
|
||||||
|
- [ ] 其他
|
||||||
|
|
||||||
|
### PR 是否包含破坏性更新?
|
||||||
|
|
||||||
|
- [ ] 是
|
||||||
|
- [ ] 否
|
||||||
|
|
||||||
|
### PR 描述
|
||||||
|
|
||||||
|
**请在下方详细描述您的 PR,包括目的、实现细节等。**
|
||||||
|
|
||||||
|
### **重要提示**
|
||||||
|
|
||||||
|
**所有 PR 都必须提交到 `alpha` 分支。请确保您的 PR 目标分支是 `alpha`。**
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
name: Publish Docker image (amd64)
|
name: Publish Docker image (alpha)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
branches:
|
||||||
- '*'
|
- alpha
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
name:
|
name:
|
||||||
description: 'reason'
|
description: "reason"
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push_to_registries:
|
push_to_registries:
|
||||||
name: Push Docker image to multiple registries
|
name: Push Docker image to multiple registries
|
||||||
@@ -18,37 +19,44 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Save version info
|
- name: Save version info
|
||||||
run: |
|
run: |
|
||||||
git describe --tags > VERSION
|
echo "alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" > VERSION
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
calciumion/new-api
|
calciumion/new-api
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=alpha
|
||||||
|
type=raw,value=alpha-{{date 'YYYYMMDD'}}-{{sha}}
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
22
.github/workflows/docker-image-arm64.yml
vendored
22
.github/workflows/docker-image-arm64.yml
vendored
@@ -1,15 +1,9 @@
|
|||||||
name: Publish Docker image (arm64)
|
name: Publish Docker image (Multi Registries)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
- '!*-alpha*'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
name:
|
|
||||||
description: 'reason'
|
|
||||||
required: false
|
|
||||||
jobs:
|
jobs:
|
||||||
push_to_registries:
|
push_to_registries:
|
||||||
name: Push Docker image to multiple registries
|
name: Push Docker image to multiple registries
|
||||||
@@ -19,26 +13,26 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Save version info
|
- name: Save version info
|
||||||
run: |
|
run: |
|
||||||
git describe --tags > VERSION
|
git describe --tags > VERSION
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -46,14 +40,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
calciumion/new-api
|
calciumion/new-api
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
13
.github/workflows/linux-release.yml
vendored
13
.github/workflows/linux-release.yml
vendored
@@ -3,6 +3,11 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
description: 'reason'
|
||||||
|
required: false
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
@@ -15,16 +20,16 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v3
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
bun-version: latest
|
||||||
- name: Build Frontend
|
- name: Build Frontend
|
||||||
env:
|
env:
|
||||||
CI: ""
|
CI: ""
|
||||||
run: |
|
run: |
|
||||||
cd web
|
cd web
|
||||||
npm install
|
bun install
|
||||||
REACT_APP_VERSION=$(git describe --tags) npm run build
|
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
|
||||||
cd ..
|
cd ..
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
|
|||||||
14
.github/workflows/macos-release.yml
vendored
14
.github/workflows/macos-release.yml
vendored
@@ -3,6 +3,11 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
description: 'reason'
|
||||||
|
required: false
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
@@ -15,16 +20,17 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v3
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
bun-version: latest
|
||||||
- name: Build Frontend
|
- name: Build Frontend
|
||||||
env:
|
env:
|
||||||
CI: ""
|
CI: ""
|
||||||
|
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||||
run: |
|
run: |
|
||||||
cd web
|
cd web
|
||||||
npm install
|
bun install
|
||||||
REACT_APP_VERSION=$(git describe --tags) npm run build
|
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
|
||||||
cd ..
|
cd ..
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
|
|||||||
21
.github/workflows/pr-target-branch-check.yml
vendored
Normal file
21
.github/workflows/pr-target-branch-check.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Check PR Branching Strategy
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-branching-strategy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Enforce branching strategy
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.base_ref }}" == "main" ]]; then
|
||||||
|
if [[ "${{ github.head_ref }}" != "alpha" ]]; then
|
||||||
|
echo "Error: Pull requests to 'main' are only allowed from the 'alpha' branch."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [[ "${{ github.base_ref }}" != "alpha" ]]; then
|
||||||
|
echo "Error: Pull requests must be targeted to the 'alpha' or 'main' branch."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Branching strategy check passed."
|
||||||
13
.github/workflows/windows-release.yml
vendored
13
.github/workflows/windows-release.yml
vendored
@@ -3,6 +3,11 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
description: 'reason'
|
||||||
|
required: false
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
@@ -18,16 +23,16 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v3
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
bun-version: latest
|
||||||
- name: Build Frontend
|
- name: Build Frontend
|
||||||
env:
|
env:
|
||||||
CI: ""
|
CI: ""
|
||||||
run: |
|
run: |
|
||||||
cd web
|
cd web
|
||||||
npm install
|
bun install
|
||||||
REACT_APP_VERSION=$(git describe --tags) npm run build
|
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
|
||||||
cd ..
|
cd ..
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,4 +9,5 @@ logs
|
|||||||
web/dist
|
web/dist
|
||||||
.env
|
.env
|
||||||
one-api
|
one-api
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
tiktoken_cache
|
||||||
@@ -24,8 +24,7 @@ RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)'" -o one-
|
|||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
|
||||||
RUN apk update \
|
RUN apk upgrade --no-cache \
|
||||||
&& apk upgrade \
|
|
||||||
&& apk add --no-cache ca-certificates tzdata ffmpeg \
|
&& apk add --no-cache ca-certificates tzdata ffmpeg \
|
||||||
&& update-ca-certificates
|
&& update-ca-certificates
|
||||||
|
|
||||||
|
|||||||
238
README.en.md
238
README.en.md
@@ -1,10 +1,13 @@
|
|||||||
|
<p align="right">
|
||||||
|
<a href="./README.md">中文</a> | <strong>English</strong>
|
||||||
|
</p>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
# New API
|
# New API
|
||||||
|
|
||||||
🍥 Next Generation LLM Gateway and AI Asset Management System
|
🍥 Next-Generation Large Model Gateway and AI Asset Management System
|
||||||
|
|
||||||
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
@@ -33,162 +36,159 @@
|
|||||||
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
|
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and relevant laws and regulations. Not to be used for illegal purposes.
|
> - This project is for personal learning purposes only, with no guarantee of stability or technical support.
|
||||||
> - This project is for personal learning only. Stability is not guaranteed, and no technical support is provided.
|
> - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**, and must not use it for illegal purposes.
|
||||||
|
> - According to the [《Interim Measures for the Management of Generative Artificial Intelligence Services》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), please do not provide any unregistered generative AI services to the public in China.
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
For detailed documentation, please visit our official Wiki: [https://docs.newapi.pro/](https://docs.newapi.pro/)
|
||||||
|
|
||||||
|
You can also access the AI-generated DeepWiki:
|
||||||
|
[](https://deepwiki.com/QuantumNous/new-api)
|
||||||
|
|
||||||
## ✨ Key Features
|
## ✨ Key Features
|
||||||
|
|
||||||
1. 🎨 New UI interface (some interfaces pending update)
|
New API offers a wide range of features, please refer to [Features Introduction](https://docs.newapi.pro/wiki/features-introduction) for details:
|
||||||
2. 🌍 Multi-language support (work in progress)
|
|
||||||
3. 🎨 Added [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface support, [Integration Guide](Midjourney.md)
|
1. 🎨 Brand new UI interface
|
||||||
4. 💰 Online recharge support, configurable in system settings:
|
2. 🌍 Multi-language support
|
||||||
- [x] EasyPay
|
3. 💰 Online recharge functionality (YiPay)
|
||||||
5. 🔍 Query usage quota by key:
|
4. 🔍 Support for querying usage quotas with keys (works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
|
||||||
- Works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)
|
5. 🔄 Compatible with the original One API database
|
||||||
6. 📑 Configurable items per page in pagination
|
6. 💵 Support for pay-per-use model pricing
|
||||||
7. 🔄 Compatible with original One API database (one-api.db)
|
7. ⚖️ Support for weighted random channel selection
|
||||||
8. 💵 Support per-request model pricing, configurable in System Settings - Operation Settings
|
8. 📈 Data dashboard (console)
|
||||||
9. ⚖️ Support channel **weighted random** selection
|
9. 🔒 Token grouping and model restrictions
|
||||||
10. 📈 Data dashboard (console)
|
10. 🤖 Support for more authorization login methods (LinuxDO, Telegram, OIDC)
|
||||||
11. 🔒 Configurable model access per token
|
11. 🔄 Support for Rerank models (Cohere and Jina), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
|
||||||
12. 🤖 Telegram authorization login support:
|
12. ⚡ Support for OpenAI Realtime API (including Azure channels), [API Documentation](https://docs.newapi.pro/api/openai-realtime)
|
||||||
1. System Settings - Configure Login Registration - Allow Telegram Login
|
13. ⚡ Support for Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
|
||||||
2. Send /setdomain command to [@Botfather](https://t.me/botfather)
|
14. Support for entering chat interface via /chat2link route
|
||||||
3. Select your bot, then enter http(s)://your-website/login
|
15. 🧠 Support for setting reasoning effort through model name suffixes:
|
||||||
4. Telegram Bot name is the bot username without @
|
1. OpenAI o-series models
|
||||||
13. 🎵 Added [Suno API](https://github.com/Suno-API/Suno-API) interface support, [Integration Guide](Suno.md)
|
- Add `-high` suffix for high reasoning effort (e.g.: `o3-mini-high`)
|
||||||
14. 🔄 Support for Rerank models, compatible with Cohere and Jina, can integrate with Dify, [Integration Guide](Rerank.md)
|
- Add `-medium` suffix for medium reasoning effort (e.g.: `o3-mini-medium`)
|
||||||
15. ⚡ **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - Support for OpenAI's Realtime API, including Azure channels
|
- Add `-low` suffix for low reasoning effort (e.g.: `o3-mini-low`)
|
||||||
16. 🧠 Support for setting reasoning effort through model name suffix:
|
2. Claude thinking models
|
||||||
- Add suffix `-high` to set high reasoning effort (e.g., `o3-mini-high`)
|
- Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`)
|
||||||
- Add suffix `-medium` to set medium reasoning effort
|
16. 🔄 Thinking-to-content functionality
|
||||||
- Add suffix `-low` to set low reasoning effort
|
17. 🔄 Model rate limiting for users
|
||||||
17. 🔄 Thinking to content option `thinking_to_content` in `Channel->Edit->Channel Extra Settings`, default is `false`, when `true`, the `reasoning_conetnt` of the thinking content will be converted to `<think>` tags and concatenated to the content returned.
|
18. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
|
||||||
|
1. Set the `Prompt Cache Ratio` option in `System Settings-Operation Settings`
|
||||||
|
2. Set `Prompt Cache Ratio` in the channel, range 0-1, e.g., setting to 0.5 means billing at 50% when cache is hit
|
||||||
|
3. Supported channels:
|
||||||
|
- [x] OpenAI
|
||||||
|
- [x] Azure
|
||||||
|
- [x] DeepSeek
|
||||||
|
- [x] Claude
|
||||||
|
|
||||||
## Model Support
|
## Model Support
|
||||||
This version additionally supports:
|
|
||||||
1. Third-party model **gps** (gpt-4-gizmo-*)
|
|
||||||
2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface, [Integration Guide](Midjourney.md)
|
|
||||||
3. Custom channels with full API URL support
|
|
||||||
4. [Suno API](https://github.com/Suno-API/Suno-API) interface, [Integration Guide](Suno.md)
|
|
||||||
5. Rerank models, supporting [Cohere](https://cohere.ai/) and [Jina](https://jina.ai/), [Integration Guide](Rerank.md)
|
|
||||||
6. Dify
|
|
||||||
|
|
||||||
You can add custom models gpt-4-gizmo-* in channels. These are third-party models and cannot be called with official OpenAI keys.
|
This version supports multiple models, please refer to [API Documentation-Relay Interface](https://docs.newapi.pro/api) for details:
|
||||||
|
|
||||||
## Additional Configurations Beyond One API
|
1. Third-party models **gpts** (gpt-4-gizmo-*)
|
||||||
- `GENERATE_DEFAULT_TOKEN`: Generate initial token for new users, default `false`
|
2. Third-party channel [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface, [API Documentation](https://docs.newapi.pro/api/midjourney-proxy-image)
|
||||||
- `STREAMING_TIMEOUT`: Set streaming response timeout, default 60 seconds
|
3. Third-party channel [Suno API](https://github.com/Suno-API/Suno-API) interface, [API Documentation](https://docs.newapi.pro/api/suno-music)
|
||||||
- `DIFY_DEBUG`: Output workflow and node info to client for Dify channel, default `true`
|
4. Custom channels, supporting full call address input
|
||||||
- `FORCE_STREAM_OPTION`: Override client stream_options parameter, default `true`
|
5. Rerank models ([Cohere](https://cohere.ai/) and [Jina](https://jina.ai/)), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
|
||||||
- `GET_MEDIA_TOKEN`: Calculate image tokens, default `true`
|
6. Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
|
||||||
- `GET_MEDIA_TOKEN_NOT_STREAM`: Calculate image tokens in non-stream mode, default `true`
|
7. Dify, currently only supports chatflow
|
||||||
- `UPDATE_TASK`: Update async tasks (Midjourney, Suno), default `true`
|
|
||||||
- `GEMINI_MODEL_MAP`: Specify Gemini model versions (v1/v1beta), format: "model:version", comma-separated
|
## Environment Variable Configuration
|
||||||
- `COHERE_SAFETY_SETTING`: Cohere model [safety settings](https://docs.cohere.com/docs/safety-modes#overview), options: `NONE`, `CONTEXTUAL`, `STRICT`, default `NONE`
|
|
||||||
- `GEMINI_VISION_MAX_IMAGE_NUM`: Gemini model maximum image number, default `16`, set to `-1` to disable
|
For detailed configuration instructions, please refer to [Installation Guide-Environment Variables Configuration](https://docs.newapi.pro/installation/environment-variables):
|
||||||
- `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default `20`
|
|
||||||
- `CRYPTO_SECRET`: Encryption key for encrypting database content
|
- `GENERATE_DEFAULT_TOKEN`: Whether to generate initial tokens for newly registered users, default is `false`
|
||||||
- `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, if not specified in channel settings, use this version, default `2024-12-01-preview`
|
- `STREAMING_TIMEOUT`: Streaming response timeout, default is 120 seconds
|
||||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`: Duration of notification limit in minutes, default `10`
|
- `DIFY_DEBUG`: Whether to output workflow and node information for Dify channels, default is `true`
|
||||||
- `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications in the specified duration, default `2`
|
- `FORCE_STREAM_OPTION`: Whether to override client stream_options parameter, default is `true`
|
||||||
|
- `GET_MEDIA_TOKEN`: Whether to count image tokens, default is `true`
|
||||||
|
- `GET_MEDIA_TOKEN_NOT_STREAM`: Whether to count image tokens in non-streaming cases, default is `true`
|
||||||
|
- `UPDATE_TASK`: Whether to update asynchronous tasks (Midjourney, Suno), default is `true`
|
||||||
|
- `COHERE_SAFETY_SETTING`: Cohere model safety settings, options are `NONE`, `CONTEXTUAL`, `STRICT`, default is `NONE`
|
||||||
|
- `GEMINI_VISION_MAX_IMAGE_NUM`: Maximum number of images for Gemini models, default is `16`
|
||||||
|
- `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default is `20`
|
||||||
|
- `CRYPTO_SECRET`: Encryption key used for encrypting database content
|
||||||
|
- `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, default is `2025-04-01-preview`
|
||||||
|
- `NOTIFICATION_LIMIT_DURATION_MINUTE`: Notification limit duration, default is `10` minutes
|
||||||
|
- `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications within the specified duration, default is `2`
|
||||||
|
- `ERROR_LOG_ENABLED=true`: Whether to record and display error logs, default is `false`
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
For detailed deployment guides, please refer to [Installation Guide-Deployment Methods](https://docs.newapi.pro/installation):
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Latest Docker image: `calciumion/new-api:latest`
|
> Latest Docker image: `calciumion/new-api:latest`
|
||||||
> Default account: root, password: 123456
|
|
||||||
|
|
||||||
### Multi-Server Deployment
|
### Multi-machine Deployment Considerations
|
||||||
- Must set `SESSION_SECRET` environment variable, otherwise login state will not be consistent across multiple servers.
|
- Environment variable `SESSION_SECRET` must be set, otherwise login status will be inconsistent across multiple machines
|
||||||
- If using a public Redis, must set `CRYPTO_SECRET` environment variable, otherwise Redis content will not be able to be obtained in multi-server deployment.
|
- If sharing Redis, `CRYPTO_SECRET` must be set, otherwise Redis content cannot be accessed across multiple machines
|
||||||
|
|
||||||
### Requirements
|
### Deployment Requirements
|
||||||
- Local database (default): SQLite (Docker deployment must mount `/data` directory)
|
- Local database (default): SQLite (Docker deployment must mount the `/data` directory)
|
||||||
- Remote database: MySQL >= 5.7.8, PgSQL >= 9.6
|
- Remote database: MySQL version >= 5.7.8, PgSQL version >= 9.6
|
||||||
|
|
||||||
### Deployment with BT Panel
|
### Deployment Methods
|
||||||
Install BT Panel (**version 9.2.0** or above) from [BT Panel Official Website](https://www.bt.cn/new/download.html), choose the stable version script to download and install.
|
|
||||||
After installation, log in to BT Panel and click Docker in the menu bar. First-time access will prompt to install Docker service. Click Install Now and follow the prompts to complete installation.
|
|
||||||
After installation, find **New-API** in the app store, click install, configure basic options to complete installation.
|
|
||||||
[Pictorial Guide](BT.md)
|
|
||||||
|
|
||||||
### Docker Deployment
|
#### Using BaoTa Panel Docker Feature
|
||||||
|
Install BaoTa Panel (version **9.2.0** or above), find **New-API** in the application store and install it.
|
||||||
|
[Tutorial with images](./docs/BT.md)
|
||||||
|
|
||||||
### Using Docker Compose (Recommended)
|
#### Using Docker Compose (Recommended)
|
||||||
```shell
|
```shell
|
||||||
# Clone project
|
# Download the project
|
||||||
git clone https://github.com/Calcium-Ion/new-api.git
|
git clone https://github.com/Calcium-Ion/new-api.git
|
||||||
cd new-api
|
cd new-api
|
||||||
# Edit docker-compose.yml as needed
|
# Edit docker-compose.yml as needed
|
||||||
# nano docker-compose.yml
|
|
||||||
# vim docker-compose.yml
|
|
||||||
# Start
|
# Start
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Update Version
|
#### Using Docker Image Directly
|
||||||
```shell
|
```shell
|
||||||
docker-compose pull
|
# Using SQLite
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Direct Docker Image Usage
|
|
||||||
```shell
|
|
||||||
# SQLite deployment:
|
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
||||||
|
|
||||||
# MySQL deployment (add -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"), modify database connection parameters as needed
|
# Using MySQL
|
||||||
# Example:
|
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Update Version
|
## Channel Retry and Cache
|
||||||
```shell
|
Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings`. It is **recommended to enable caching**.
|
||||||
# Pull the latest image
|
|
||||||
docker pull calciumion/new-api:latest
|
|
||||||
# Stop and remove the old container
|
|
||||||
docker stop new-api
|
|
||||||
docker rm new-api
|
|
||||||
# Run the new container with the same parameters as before
|
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, you can use Watchtower for automatic updates (not recommended, may cause database incompatibility):
|
### Cache Configuration Method
|
||||||
```shell
|
1. `REDIS_CONN_STRING`: Set Redis as cache
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
2. `MEMORY_CACHE_ENABLED`: Enable memory cache (no need to set manually if Redis is set)
|
||||||
```
|
|
||||||
|
|
||||||
## Channel Retry
|
## API Documentation
|
||||||
Channel retry is implemented, configurable in `Settings->Operation Settings->General Settings`. **Cache recommended**.
|
|
||||||
First retry uses same priority, second retry uses next priority, and so on.
|
|
||||||
|
|
||||||
### Cache Configuration
|
For detailed API documentation, please refer to [API Documentation](https://docs.newapi.pro/api):
|
||||||
1. `REDIS_CONN_STRING`: Use Redis as cache
|
|
||||||
+ Example: `REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
|
|
||||||
2. `MEMORY_CACHE_ENABLED`: Enable memory cache, default `false`
|
|
||||||
+ Example: `MEMORY_CACHE_ENABLED=true`
|
|
||||||
|
|
||||||
### Why Some Errors Don't Retry
|
- [Chat API](https://docs.newapi.pro/api/openai-chat)
|
||||||
Error codes 400, 504, 524 won't retry
|
- [Image API](https://docs.newapi.pro/api/openai-image)
|
||||||
### To Enable Retry for 400
|
- [Rerank API](https://docs.newapi.pro/api/jinaai-rerank)
|
||||||
In `Channel->Edit`, set `Status Code Override` to:
|
- [Realtime API](https://docs.newapi.pro/api/openai-realtime)
|
||||||
```json
|
- [Claude Chat API (messages)](https://docs.newapi.pro/api/anthropic-chat)
|
||||||
{
|
|
||||||
"400": "500"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Guides
|
|
||||||
- [Midjourney Integration](Midjourney.md)
|
|
||||||
- [Suno Integration](Suno.md)
|
|
||||||
|
|
||||||
## Related Projects
|
## Related Projects
|
||||||
- [One API](https://github.com/songquanpeng/one-api): Original project
|
- [One API](https://github.com/songquanpeng/one-api): Original project
|
||||||
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
|
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
|
||||||
- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-gen AI B/C solution
|
- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-generation AI one-stop B/C-end solution
|
||||||
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota by key
|
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota with key
|
||||||
|
|
||||||
|
Other projects based on New API:
|
||||||
|
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon): High-performance optimized version of New API
|
||||||
|
- [VoAPI](https://github.com/VoAPI/VoAPI): Frontend beautified version based on New API
|
||||||
|
|
||||||
|
## Help and Support
|
||||||
|
|
||||||
|
If you have any questions, please refer to [Help and Support](https://docs.newapi.pro/support):
|
||||||
|
- [Community Interaction](https://docs.newapi.pro/support/community-interaction)
|
||||||
|
- [Issue Feedback](https://docs.newapi.pro/support/feedback-issues)
|
||||||
|
- [FAQ](https://docs.newapi.pro/support/faq)
|
||||||
|
|
||||||
## 🌟 Star History
|
## 🌟 Star History
|
||||||
|
|
||||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||||
|
|||||||
238
README.md
238
README.md
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
# New API
|
# New API
|
||||||
|
|
||||||
|
|
||||||
🍥新一代大模型网关与AI资产管理系统
|
🍥新一代大模型网关与AI资产管理系统
|
||||||
|
|
||||||
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
@@ -37,184 +36,157 @@
|
|||||||
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
|
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
|
||||||
> - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
|
> - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
|
||||||
|
> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
||||||
> - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
> - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||||
|
|
||||||
|
## 📚 文档
|
||||||
|
|
||||||
|
详细文档请访问我们的官方Wiki:[https://docs.newapi.pro/](https://docs.newapi.pro/)
|
||||||
|
|
||||||
|
也可访问AI生成的DeepWiki:
|
||||||
|
[](https://deepwiki.com/QuantumNous/new-api)
|
||||||
|
|
||||||
## ✨ 主要特性
|
## ✨ 主要特性
|
||||||
|
|
||||||
1. 🎨 全新的UI界面(部分界面还待更新)
|
New API提供了丰富的功能,详细特性请参考[特性说明](https://docs.newapi.pro/wiki/features-introduction):
|
||||||
2. 🌍 多语言支持(待完善)
|
|
||||||
3. 🎨 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口支持,[对接文档](Midjourney.md)
|
1. 🎨 全新的UI界面
|
||||||
4. 💰 支持在线充值功能,可在系统设置中设置:
|
2. 🌍 多语言支持
|
||||||
- [x] 易支付
|
3. 💰 支持在线充值功能(易支付)
|
||||||
5. 🔍 支持用key查询使用额度:
|
4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
|
||||||
- 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用
|
5. 🔄 兼容原版One API的数据库
|
||||||
6. 📑 分页支持选择每页显示数量
|
6. 💵 支持模型按次数收费
|
||||||
7. 🔄 兼容原版One API的数据库,可直接使用原版数据库(one-api.db)
|
7. ⚖️ 支持渠道加权随机
|
||||||
8. 💵 支持模型按次数收费,可在 系统设置-运营设置 中设置
|
8. 📈 数据看板(控制台)
|
||||||
9. ⚖️ 支持渠道**加权随机**
|
9. 🔒 令牌分组、模型限制
|
||||||
10. 📈 数据看板(控制台)
|
10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC)
|
||||||
11. 🔒 可设置令牌能调用的模型
|
11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
|
||||||
12. 🤖 支持Telegram授权登录:
|
12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime)
|
||||||
1. 系统设置-配置登录注册-允许通过Telegram登录
|
13. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
|
||||||
2. 对[@Botfather](https://t.me/botfather)输入指令/setdomain
|
14. 支持使用路由/chat2link进入聊天界面
|
||||||
3. 选择你的bot,然后输入http(s)://你的网站地址/login
|
15. 🧠 支持通过模型名称后缀设置 reasoning effort:
|
||||||
4. Telegram Bot 名称是bot username 去掉@后的字符串
|
1. OpenAI o系列模型
|
||||||
13. 🎵 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口支持,[对接文档](Suno.md)
|
- 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`)
|
||||||
14. 🔄 支持Rerank模型,目前兼容Cohere和Jina,可接入Dify,[对接文档](Rerank.md)
|
- 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`)
|
||||||
15. ⚡ **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API,支持Azure渠道
|
- 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`)
|
||||||
16. 支持使用路由/chat2link 进入聊天界面
|
2. Claude 思考模型
|
||||||
17. 🧠 支持通过模型名称后缀设置 reasoning effort:
|
- 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`)
|
||||||
- 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`)
|
16. 🔄 思考转内容功能
|
||||||
- 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`)
|
17. 🔄 针对用户的模型限流功能
|
||||||
- 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`)
|
18. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
|
||||||
18. 🔄 思考转内容,支持在 `渠道-编辑-渠道额外设置` 中设置 `thinking_to_content` 选项,默认`false`,开启后会将思考内容`reasoning_conetnt`转换为`<think>`标签拼接到内容中返回。
|
1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项
|
||||||
|
2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费
|
||||||
|
3. 支持的渠道:
|
||||||
|
- [x] OpenAI
|
||||||
|
- [x] Azure
|
||||||
|
- [x] DeepSeek
|
||||||
|
- [x] Claude
|
||||||
|
|
||||||
## 模型支持
|
## 模型支持
|
||||||
此版本额外支持以下模型:
|
|
||||||
1. 第三方模型 **gps** (gpt-4-gizmo-*)
|
|
||||||
2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md)
|
|
||||||
3. 自定义渠道,支持填入完整调用地址
|
|
||||||
4. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md)
|
|
||||||
5. Rerank模型,目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/),[对接文档](Rerank.md)
|
|
||||||
6. Dify
|
|
||||||
|
|
||||||
您可以在渠道中添加自定义模型gpt-4-gizmo-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。
|
此版本支持多种模型,详情请参考[接口文档-中继接口](https://docs.newapi.pro/api):
|
||||||
|
|
||||||
## 比原版One API多出的配置
|
1. 第三方模型 **gpts** (gpt-4-gizmo-*)
|
||||||
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`。
|
2. 第三方渠道[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[接口文档](https://docs.newapi.pro/api/midjourney-proxy-image)
|
||||||
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 60 秒。
|
3. 第三方渠道[Suno API](https://github.com/Suno-API/Suno-API)接口,[接口文档](https://docs.newapi.pro/api/suno-music)
|
||||||
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。
|
4. 自定义渠道,支持填入完整调用地址
|
||||||
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,请求上游返回流模式usage,默认为 `true`,建议开启,不影响客户端传入stream_options参数返回结果。
|
5. Rerank模型([Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
|
||||||
- `GET_MEDIA_TOKEN`:是否统计图片token,默认为 `true`,关闭后将不再在本地计算图片token,可能会导致和上游计费不同,此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。
|
6. Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
|
||||||
- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`)情况下统计图片token,默认为 `true`。
|
7. Dify,当前仅支持chatflow
|
||||||
- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认为 `true`,关闭后将不会更新任务进度。
|
|
||||||
- `GEMINI_MODEL_MAP`:Gemini模型指定版本(v1/v1beta),使用"模型:版本"指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置(v1beta)
|
## 环境变量配置
|
||||||
- `COHERE_SAFETY_SETTING`:Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认为 `NONE`。
|
|
||||||
- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认为 `16`,设置为 `-1` 则不限制。
|
详细配置说明请参考[安装指南-环境变量配置](https://docs.newapi.pro/installation/environment-variables):
|
||||||
- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位 MB,默认为 `20`。
|
|
||||||
- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容。
|
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
|
||||||
- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,如果渠道设置中未指定API版本,则使用此版本,默认为 `2024-12-01-preview`
|
- `STREAMING_TIMEOUT`:流式回复超时时间,默认120秒
|
||||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制的持续时间(分钟),默认为 `10`。
|
- `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true`
|
||||||
- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认为 `2`。
|
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,默认 `true`
|
||||||
|
- `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true`
|
||||||
|
- `GET_MEDIA_TOKEN_NOT_STREAM`:非流情况下是否统计图片token,默认 `true`
|
||||||
|
- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认 `true`
|
||||||
|
- `COHERE_SAFETY_SETTING`:Cohere模型安全设置,可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认 `NONE`
|
||||||
|
- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16`
|
||||||
|
- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20`
|
||||||
|
- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容
|
||||||
|
- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2025-04-01-preview`
|
||||||
|
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制持续时间,默认 `10`分钟
|
||||||
|
- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
|
||||||
|
- `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false`
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
|
详细部署指南请参考[安装指南-部署方式](https://docs.newapi.pro/installation):
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> 最新版Docker镜像:`calciumion/new-api:latest`
|
> 最新版Docker镜像:`calciumion/new-api:latest`
|
||||||
> 默认账号root 密码123456
|
|
||||||
|
|
||||||
### 多机部署
|
### 多机部署注意事项
|
||||||
- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致。
|
- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致
|
||||||
- 如果公用Redis,必须设置 `CRYPTO_SECRET`,否则会导致多机部署时Redis内容无法获取。
|
- 如果公用Redis,必须设置 `CRYPTO_SECRET`,否则会导致多机部署时Redis内容无法获取
|
||||||
|
|
||||||
### 部署要求
|
### 部署要求
|
||||||
- 本地数据库(默认):SQLite(Docker 部署默认使用 SQLite,必须挂载 `/data` 目录到宿主机)
|
- 本地数据库(默认):SQLite(Docker部署必须挂载`/data`目录)
|
||||||
- 远程数据库:MySQL 版本 >= 5.7.8,PgSQL 版本 >= 9.6
|
- 远程数据库:MySQL版本 >= 5.7.8,PgSQL版本 >= 9.6
|
||||||
|
|
||||||
### 使用宝塔面板Docker功能部署
|
### 部署方式
|
||||||
安装宝塔面板 (**9.2.0版本**及以上),前往 [宝塔面板](https://www.bt.cn/new/download.html) 官网,选择正式版的脚本下载安装
|
|
||||||
安装后登录宝塔面板,在菜单栏中点击 Docker ,首次进入会提示安装 Docker 服务,点击立即安装,按提示完成安装
|
|
||||||
安装完成后在应用商店中找到 **New-API** ,点击安装,配置基本选项 即可完成安装
|
|
||||||
[图文教程](BT.md)
|
|
||||||
|
|
||||||
### 基于 Docker 进行部署
|
#### 使用宝塔面板Docker功能部署
|
||||||
|
安装宝塔面板(**9.2.0版本**及以上),在应用商店中找到**New-API**安装即可。
|
||||||
|
[图文教程](./docs/BT.md)
|
||||||
|
|
||||||
> [!TIP]
|
#### 使用Docker Compose部署(推荐)
|
||||||
> 默认管理员账号root 密码123456
|
|
||||||
|
|
||||||
### 使用 Docker Compose 部署(推荐)
|
|
||||||
```shell
|
```shell
|
||||||
# 下载项目
|
# 下载项目
|
||||||
git clone https://github.com/Calcium-Ion/new-api.git
|
git clone https://github.com/Calcium-Ion/new-api.git
|
||||||
cd new-api
|
cd new-api
|
||||||
# 按需编辑 docker-compose.yml
|
# 按需编辑docker-compose.yml
|
||||||
# nano docker-compose.yml
|
|
||||||
# vim docker-compose.yml
|
|
||||||
# 启动
|
# 启动
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 更新版本
|
#### 直接使用Docker镜像
|
||||||
```shell
|
```shell
|
||||||
docker-compose pull
|
# 使用SQLite
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 直接使用 Docker 镜像
|
|
||||||
```shell
|
|
||||||
# 使用 SQLite 的部署命令:
|
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
||||||
|
|
||||||
# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数。
|
# 使用MySQL
|
||||||
# 例如:
|
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 更新版本
|
## 渠道重试与缓存
|
||||||
```shell
|
渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
|
||||||
# 拉取最新镜像
|
|
||||||
docker pull calciumion/new-api:latest
|
|
||||||
# 停止并删除旧容器
|
|
||||||
docker stop new-api
|
|
||||||
docker rm new-api
|
|
||||||
# 使用相同参数运行新容器
|
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
或者使用 Watchtower 自动更新(不推荐,可能会导致数据库不兼容):
|
|
||||||
```shell
|
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
|
||||||
```
|
|
||||||
|
|
||||||
## 渠道重试
|
|
||||||
渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
|
|
||||||
如果开启了重试功能,第一次重试使用同优先级,第二次重试使用下一个优先级,以此类推。
|
|
||||||
### 缓存设置方法
|
### 缓存设置方法
|
||||||
1. `REDIS_CONN_STRING`:设置之后将使用 Redis 作为缓存使用。
|
1. `REDIS_CONN_STRING`:设置Redis作为缓存
|
||||||
+ 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
|
2. `MEMORY_CACHE_ENABLED`:启用内存缓存(设置了Redis则无需手动设置)
|
||||||
2. `MEMORY_CACHE_ENABLED`:启用内存缓存(如果设置了`REDIS_CONN_STRING`,则无需手动设置),会导致用户额度的更新存在一定的延迟,可选值为 `true` 和 `false`,未设置则默认为 `false`。
|
|
||||||
+ 例子:`MEMORY_CACHE_ENABLED=true`
|
|
||||||
### 为什么有的时候没有重试
|
|
||||||
这些错误码不会重试:400,504,524
|
|
||||||
### 我想让400也重试
|
|
||||||
在`渠道->编辑`中,将`状态码复写`改为
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"400": "500"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
可以实现400错误转为500错误,从而重试
|
|
||||||
|
|
||||||
## Midjourney接口设置文档
|
## 接口文档
|
||||||
[对接文档](Midjourney.md)
|
|
||||||
|
|
||||||
## Suno接口设置文档
|
详细接口文档请参考[接口文档](https://docs.newapi.pro/api):
|
||||||
[对接文档](Suno.md)
|
|
||||||
|
|
||||||
## 界面截图
|
- [聊天接口(Chat)](https://docs.newapi.pro/api/openai-chat)
|
||||||

|
- [图像接口(Image)](https://docs.newapi.pro/api/openai-image)
|
||||||
|
- [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
|
||||||
|
- [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime)
|
||||||

|
- [Claude聊天接口(messages)](https://docs.newapi.pro/api/anthropic-chat)
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 交流群
|
|
||||||
<img src="https://github.com/user-attachments/assets/9ca0bc82-e057-4230-a28d-9f198fa022e3" width="200">
|
|
||||||
|
|
||||||
## 相关项目
|
## 相关项目
|
||||||
- [One API](https://github.com/songquanpeng/one-api):原版项目
|
- [One API](https://github.com/songquanpeng/one-api):原版项目
|
||||||
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持
|
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持
|
||||||
- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代 AI 一站式 B/C 端解决方案
|
- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代AI一站式B/C端解决方案
|
||||||
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度
|
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度
|
||||||
|
|
||||||
其他基于New API的项目:
|
其他基于New API的项目:
|
||||||
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能优化版,并支持Claude格式
|
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能优化版
|
||||||
- [VoAPI](https://github.com/VoAPI/VoAPI):基于New API的闭源项目
|
|
||||||
|
## 帮助支持
|
||||||
|
|
||||||
|
如有问题,请参考[帮助支持](https://docs.newapi.pro/support):
|
||||||
|
- [社区交流](https://docs.newapi.pro/support/community-interaction)
|
||||||
|
- [反馈问题](https://docs.newapi.pro/support/feedback-issues)
|
||||||
|
- [常见问题](https://docs.newapi.pro/support/faq)
|
||||||
|
|
||||||
## 🌟 Star History
|
## 🌟 Star History
|
||||||
|
|
||||||
|
|||||||
71
common/api_type.go
Normal file
71
common/api_type.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "one-api/constant"
|
||||||
|
|
||||||
|
func ChannelType2APIType(channelType int) (int, bool) {
|
||||||
|
apiType := -1
|
||||||
|
switch channelType {
|
||||||
|
case constant.ChannelTypeOpenAI:
|
||||||
|
apiType = constant.APITypeOpenAI
|
||||||
|
case constant.ChannelTypeAnthropic:
|
||||||
|
apiType = constant.APITypeAnthropic
|
||||||
|
case constant.ChannelTypeBaidu:
|
||||||
|
apiType = constant.APITypeBaidu
|
||||||
|
case constant.ChannelTypePaLM:
|
||||||
|
apiType = constant.APITypePaLM
|
||||||
|
case constant.ChannelTypeZhipu:
|
||||||
|
apiType = constant.APITypeZhipu
|
||||||
|
case constant.ChannelTypeAli:
|
||||||
|
apiType = constant.APITypeAli
|
||||||
|
case constant.ChannelTypeXunfei:
|
||||||
|
apiType = constant.APITypeXunfei
|
||||||
|
case constant.ChannelTypeAIProxyLibrary:
|
||||||
|
apiType = constant.APITypeAIProxyLibrary
|
||||||
|
case constant.ChannelTypeTencent:
|
||||||
|
apiType = constant.APITypeTencent
|
||||||
|
case constant.ChannelTypeGemini:
|
||||||
|
apiType = constant.APITypeGemini
|
||||||
|
case constant.ChannelTypeZhipu_v4:
|
||||||
|
apiType = constant.APITypeZhipuV4
|
||||||
|
case constant.ChannelTypeOllama:
|
||||||
|
apiType = constant.APITypeOllama
|
||||||
|
case constant.ChannelTypePerplexity:
|
||||||
|
apiType = constant.APITypePerplexity
|
||||||
|
case constant.ChannelTypeAws:
|
||||||
|
apiType = constant.APITypeAws
|
||||||
|
case constant.ChannelTypeCohere:
|
||||||
|
apiType = constant.APITypeCohere
|
||||||
|
case constant.ChannelTypeDify:
|
||||||
|
apiType = constant.APITypeDify
|
||||||
|
case constant.ChannelTypeJina:
|
||||||
|
apiType = constant.APITypeJina
|
||||||
|
case constant.ChannelCloudflare:
|
||||||
|
apiType = constant.APITypeCloudflare
|
||||||
|
case constant.ChannelTypeSiliconFlow:
|
||||||
|
apiType = constant.APITypeSiliconFlow
|
||||||
|
case constant.ChannelTypeVertexAi:
|
||||||
|
apiType = constant.APITypeVertexAi
|
||||||
|
case constant.ChannelTypeMistral:
|
||||||
|
apiType = constant.APITypeMistral
|
||||||
|
case constant.ChannelTypeDeepSeek:
|
||||||
|
apiType = constant.APITypeDeepSeek
|
||||||
|
case constant.ChannelTypeMokaAI:
|
||||||
|
apiType = constant.APITypeMokaAI
|
||||||
|
case constant.ChannelTypeVolcEngine:
|
||||||
|
apiType = constant.APITypeVolcEngine
|
||||||
|
case constant.ChannelTypeBaiduV2:
|
||||||
|
apiType = constant.APITypeBaiduV2
|
||||||
|
case constant.ChannelTypeOpenRouter:
|
||||||
|
apiType = constant.APITypeOpenRouter
|
||||||
|
case constant.ChannelTypeXinference:
|
||||||
|
apiType = constant.APITypeXinference
|
||||||
|
case constant.ChannelTypeXai:
|
||||||
|
apiType = constant.APITypeXai
|
||||||
|
case constant.ChannelTypeCoze:
|
||||||
|
apiType = constant.APITypeCoze
|
||||||
|
}
|
||||||
|
if apiType == -1 {
|
||||||
|
return constant.APITypeOpenAI, false
|
||||||
|
}
|
||||||
|
return apiType, true
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
//"os"
|
||||||
"strconv"
|
//"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,8 +15,9 @@ var SystemName = "New API"
|
|||||||
var Footer = ""
|
var Footer = ""
|
||||||
var Logo = ""
|
var Logo = ""
|
||||||
var TopUpLink = ""
|
var TopUpLink = ""
|
||||||
var ChatLink = ""
|
|
||||||
var ChatLink2 = ""
|
// var ChatLink = ""
|
||||||
|
// var ChatLink2 = ""
|
||||||
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
||||||
var DisplayInCurrencyEnabled = true
|
var DisplayInCurrencyEnabled = true
|
||||||
var DisplayTokenStatEnabled = true
|
var DisplayTokenStatEnabled = true
|
||||||
@@ -61,9 +62,13 @@ var EmailDomainWhitelist = []string{
|
|||||||
"yahoo.com",
|
"yahoo.com",
|
||||||
"foxmail.com",
|
"foxmail.com",
|
||||||
}
|
}
|
||||||
|
var EmailLoginAuthServerList = []string{
|
||||||
|
"smtp.sendcloud.net",
|
||||||
|
"smtp.azurecomm.net",
|
||||||
|
}
|
||||||
|
|
||||||
var DebugEnabled = os.Getenv("DEBUG") == "true"
|
var DebugEnabled bool
|
||||||
var MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true"
|
var MemoryCacheEnabled bool
|
||||||
|
|
||||||
var LogConsumeEnabled = true
|
var LogConsumeEnabled = true
|
||||||
|
|
||||||
@@ -76,7 +81,6 @@ var SMTPToken = ""
|
|||||||
|
|
||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
var GitHubClientSecret = ""
|
var GitHubClientSecret = ""
|
||||||
|
|
||||||
var LinuxDOClientId = ""
|
var LinuxDOClientId = ""
|
||||||
var LinuxDOClientSecret = ""
|
var LinuxDOClientSecret = ""
|
||||||
|
|
||||||
@@ -103,22 +107,22 @@ var RetryTimes = 0
|
|||||||
|
|
||||||
//var RootUserEmail = ""
|
//var RootUserEmail = ""
|
||||||
|
|
||||||
var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
var IsMasterNode bool
|
||||||
|
|
||||||
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
var requestInterval int
|
||||||
var RequestInterval = time.Duration(requestInterval) * time.Second
|
var RequestInterval time.Duration
|
||||||
|
|
||||||
var SyncFrequency = GetEnvOrDefault("SYNC_FREQUENCY", 60) // unit is second
|
var SyncFrequency int // unit is second
|
||||||
|
|
||||||
var BatchUpdateEnabled = false
|
var BatchUpdateEnabled = false
|
||||||
var BatchUpdateInterval = GetEnvOrDefault("BATCH_UPDATE_INTERVAL", 5)
|
var BatchUpdateInterval int
|
||||||
|
|
||||||
var RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0) // unit is second
|
var RelayTimeout int // unit is second
|
||||||
|
|
||||||
var GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE")
|
var GeminiSafetySetting string
|
||||||
|
|
||||||
// https://docs.cohere.com/docs/safety-modes Type; NONE/CONTEXTUAL/STRICT
|
// https://docs.cohere.com/docs/safety-modes Type; NONE/CONTEXTUAL/STRICT
|
||||||
var CohereSafetySetting = GetEnvOrDefaultString("COHERE_SAFETY_SETTING", "NONE")
|
var CohereSafetySetting string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RequestIdKey = "X-Oneapi-Request-Id"
|
RequestIdKey = "X-Oneapi-Request-Id"
|
||||||
@@ -145,13 +149,13 @@ var (
|
|||||||
// All duration's unit is seconds
|
// All duration's unit is seconds
|
||||||
// Shouldn't larger then RateLimitKeyExpirationDuration
|
// Shouldn't larger then RateLimitKeyExpirationDuration
|
||||||
var (
|
var (
|
||||||
GlobalApiRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_API_RATE_LIMIT_ENABLE", true)
|
GlobalApiRateLimitEnable bool
|
||||||
GlobalApiRateLimitNum = GetEnvOrDefault("GLOBAL_API_RATE_LIMIT", 180)
|
GlobalApiRateLimitNum int
|
||||||
GlobalApiRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_API_RATE_LIMIT_DURATION", 180))
|
GlobalApiRateLimitDuration int64
|
||||||
|
|
||||||
GlobalWebRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_WEB_RATE_LIMIT_ENABLE", true)
|
GlobalWebRateLimitEnable bool
|
||||||
GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 60)
|
GlobalWebRateLimitNum int
|
||||||
GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180))
|
GlobalWebRateLimitDuration int64
|
||||||
|
|
||||||
UploadRateLimitNum = 10
|
UploadRateLimitNum = 10
|
||||||
UploadRateLimitDuration int64 = 60
|
UploadRateLimitDuration int64 = 60
|
||||||
@@ -189,101 +193,3 @@ const (
|
|||||||
ChannelStatusManuallyDisabled = 2 // also don't use 0
|
ChannelStatusManuallyDisabled = 2 // also don't use 0
|
||||||
ChannelStatusAutoDisabled = 3
|
ChannelStatusAutoDisabled = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
ChannelTypeUnknown = 0
|
|
||||||
ChannelTypeOpenAI = 1
|
|
||||||
ChannelTypeMidjourney = 2
|
|
||||||
ChannelTypeAzure = 3
|
|
||||||
ChannelTypeOllama = 4
|
|
||||||
ChannelTypeMidjourneyPlus = 5
|
|
||||||
ChannelTypeOpenAIMax = 6
|
|
||||||
ChannelTypeOhMyGPT = 7
|
|
||||||
ChannelTypeCustom = 8
|
|
||||||
ChannelTypeAILS = 9
|
|
||||||
ChannelTypeAIProxy = 10
|
|
||||||
ChannelTypePaLM = 11
|
|
||||||
ChannelTypeAPI2GPT = 12
|
|
||||||
ChannelTypeAIGC2D = 13
|
|
||||||
ChannelTypeAnthropic = 14
|
|
||||||
ChannelTypeBaidu = 15
|
|
||||||
ChannelTypeZhipu = 16
|
|
||||||
ChannelTypeAli = 17
|
|
||||||
ChannelTypeXunfei = 18
|
|
||||||
ChannelType360 = 19
|
|
||||||
ChannelTypeOpenRouter = 20
|
|
||||||
ChannelTypeAIProxyLibrary = 21
|
|
||||||
ChannelTypeFastGPT = 22
|
|
||||||
ChannelTypeTencent = 23
|
|
||||||
ChannelTypeGemini = 24
|
|
||||||
ChannelTypeMoonshot = 25
|
|
||||||
ChannelTypeZhipu_v4 = 26
|
|
||||||
ChannelTypePerplexity = 27
|
|
||||||
ChannelTypeLingYiWanWu = 31
|
|
||||||
ChannelTypeAws = 33
|
|
||||||
ChannelTypeCohere = 34
|
|
||||||
ChannelTypeMiniMax = 35
|
|
||||||
ChannelTypeSunoAPI = 36
|
|
||||||
ChannelTypeDify = 37
|
|
||||||
ChannelTypeJina = 38
|
|
||||||
ChannelCloudflare = 39
|
|
||||||
ChannelTypeSiliconFlow = 40
|
|
||||||
ChannelTypeVertexAi = 41
|
|
||||||
ChannelTypeMistral = 42
|
|
||||||
ChannelTypeDeepSeek = 43
|
|
||||||
ChannelTypeMokaAI = 44
|
|
||||||
ChannelTypeVolcEngine = 45
|
|
||||||
ChannelTypeBaiduV2 = 46
|
|
||||||
ChannelTypeDummy // this one is only for count, do not add any channel after this
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
var ChannelBaseURLs = []string{
|
|
||||||
"", // 0
|
|
||||||
"https://api.openai.com", // 1
|
|
||||||
"https://oa.api2d.net", // 2
|
|
||||||
"", // 3
|
|
||||||
"http://localhost:11434", // 4
|
|
||||||
"https://api.openai-sb.com", // 5
|
|
||||||
"https://api.openaimax.com", // 6
|
|
||||||
"https://api.ohmygpt.com", // 7
|
|
||||||
"", // 8
|
|
||||||
"https://api.caipacity.com", // 9
|
|
||||||
"https://api.aiproxy.io", // 10
|
|
||||||
"", // 11
|
|
||||||
"https://api.api2gpt.com", // 12
|
|
||||||
"https://api.aigc2d.com", // 13
|
|
||||||
"https://api.anthropic.com", // 14
|
|
||||||
"https://aip.baidubce.com", // 15
|
|
||||||
"https://open.bigmodel.cn", // 16
|
|
||||||
"https://dashscope.aliyuncs.com", // 17
|
|
||||||
"", // 18
|
|
||||||
"https://api.360.cn", // 19
|
|
||||||
"https://openrouter.ai/api", // 20
|
|
||||||
"https://api.aiproxy.io", // 21
|
|
||||||
"https://fastgpt.run/api/openapi", // 22
|
|
||||||
"https://hunyuan.tencentcloudapi.com", //23
|
|
||||||
"https://generativelanguage.googleapis.com", //24
|
|
||||||
"https://api.moonshot.cn", //25
|
|
||||||
"https://open.bigmodel.cn", //26
|
|
||||||
"https://api.perplexity.ai", //27
|
|
||||||
"", //28
|
|
||||||
"", //29
|
|
||||||
"", //30
|
|
||||||
"https://api.lingyiwanwu.com", //31
|
|
||||||
"", //32
|
|
||||||
"", //33
|
|
||||||
"https://api.cohere.ai", //34
|
|
||||||
"https://api.minimax.chat", //35
|
|
||||||
"", //36
|
|
||||||
"", //37
|
|
||||||
"https://api.jina.ai", //38
|
|
||||||
"https://api.cloudflare.com", //39
|
|
||||||
"https://api.siliconflow.cn", //40
|
|
||||||
"", //41
|
|
||||||
"https://api.mistral.ai", //42
|
|
||||||
"https://api.deepseek.com", //43
|
|
||||||
"https://api.moka.ai", //44
|
|
||||||
"https://ark.cn-beijing.volces.com", //45
|
|
||||||
"https://qianfan.baidubce.com", //46
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ var fieldReplacer = strings.NewReplacer(
|
|||||||
"\r", "\\r")
|
"\r", "\\r")
|
||||||
|
|
||||||
var dataReplacer = strings.NewReplacer(
|
var dataReplacer = strings.NewReplacer(
|
||||||
"\n", "\ndata:",
|
"\n", "\n",
|
||||||
"\r", "\\r")
|
"\r", "\\r")
|
||||||
|
|
||||||
type CustomEvent struct {
|
type CustomEvent struct {
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
const (
|
||||||
|
DatabaseTypeMySQL = "mysql"
|
||||||
|
DatabaseTypeSQLite = "sqlite"
|
||||||
|
DatabaseTypePostgreSQL = "postgres"
|
||||||
|
)
|
||||||
|
|
||||||
var UsingSQLite = false
|
var UsingSQLite = false
|
||||||
var UsingPostgreSQL = false
|
var UsingPostgreSQL = false
|
||||||
|
var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries
|
||||||
var UsingMySQL = false
|
var UsingMySQL = false
|
||||||
var UsingClickHouse = false
|
var UsingClickHouse = false
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -79,7 +80,7 @@ func SendEmail(subject string, receiver string, content string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if isOutlookServer(SMTPAccount) || SMTPServer == "smtp.azurecomm.net" {
|
} else if isOutlookServer(SMTPAccount) || slices.Contains(EmailLoginAuthServerList, SMTPServer) {
|
||||||
auth = LoginAuth(SMTPAccount, SMTPToken)
|
auth = LoginAuth(SMTPAccount, SMTPToken)
|
||||||
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
41
common/endpoint_type.go
Normal file
41
common/endpoint_type.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "one-api/constant"
|
||||||
|
|
||||||
|
// GetEndpointTypesByChannelType 获取渠道最优先端点类型(所有的渠道都支持 OpenAI 端点)
|
||||||
|
func GetEndpointTypesByChannelType(channelType int, modelName string) []constant.EndpointType {
|
||||||
|
var endpointTypes []constant.EndpointType
|
||||||
|
switch channelType {
|
||||||
|
case constant.ChannelTypeJina:
|
||||||
|
endpointTypes = []constant.EndpointType{constant.EndpointTypeJinaRerank}
|
||||||
|
//case constant.ChannelTypeMidjourney, constant.ChannelTypeMidjourneyPlus:
|
||||||
|
// endpointTypes = []constant.EndpointType{constant.EndpointTypeMidjourney}
|
||||||
|
//case constant.ChannelTypeSunoAPI:
|
||||||
|
// endpointTypes = []constant.EndpointType{constant.EndpointTypeSuno}
|
||||||
|
//case constant.ChannelTypeKling:
|
||||||
|
// endpointTypes = []constant.EndpointType{constant.EndpointTypeKling}
|
||||||
|
//case constant.ChannelTypeJimeng:
|
||||||
|
// endpointTypes = []constant.EndpointType{constant.EndpointTypeJimeng}
|
||||||
|
case constant.ChannelTypeAws:
|
||||||
|
fallthrough
|
||||||
|
case constant.ChannelTypeAnthropic:
|
||||||
|
endpointTypes = []constant.EndpointType{constant.EndpointTypeAnthropic, constant.EndpointTypeOpenAI}
|
||||||
|
case constant.ChannelTypeVertexAi:
|
||||||
|
fallthrough
|
||||||
|
case constant.ChannelTypeGemini:
|
||||||
|
endpointTypes = []constant.EndpointType{constant.EndpointTypeGemini, constant.EndpointTypeOpenAI}
|
||||||
|
case constant.ChannelTypeOpenRouter: // OpenRouter 只支持 OpenAI 端点
|
||||||
|
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
|
||||||
|
default:
|
||||||
|
if IsOpenAIResponseOnlyModel(modelName) {
|
||||||
|
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIResponse}
|
||||||
|
} else {
|
||||||
|
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if IsImageGenerationModel(modelName) {
|
||||||
|
// add to first
|
||||||
|
endpointTypes = append([]constant.EndpointType{constant.EndpointTypeImageGeneration}, endpointTypes...)
|
||||||
|
}
|
||||||
|
return endpointTypes
|
||||||
|
}
|
||||||
@@ -2,10 +2,11 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"io"
|
"io"
|
||||||
|
"one-api/constant"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const KeyRequestBody = "key_request_body"
|
const KeyRequestBody = "key_request_body"
|
||||||
@@ -31,7 +32,7 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
|||||||
}
|
}
|
||||||
contentType := c.Request.Header.Get("Content-Type")
|
contentType := c.Request.Header.Get("Content-Type")
|
||||||
if strings.HasPrefix(contentType, "application/json") {
|
if strings.HasPrefix(contentType, "application/json") {
|
||||||
err = json.Unmarshal(requestBody, &v)
|
err = UnmarshalJson(requestBody, &v)
|
||||||
} else {
|
} else {
|
||||||
// skip for now
|
// skip for now
|
||||||
// TODO: someday non json request have variant model, we will need to implementation this
|
// TODO: someday non json request have variant model, we will need to implementation this
|
||||||
@@ -43,3 +44,45 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
|||||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetContextKey(c *gin.Context, key constant.ContextKey, value any) {
|
||||||
|
c.Set(string(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextKey(c *gin.Context, key constant.ContextKey) (any, bool) {
|
||||||
|
return c.Get(string(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextKeyString(c *gin.Context, key constant.ContextKey) string {
|
||||||
|
return c.GetString(string(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextKeyInt(c *gin.Context, key constant.ContextKey) int {
|
||||||
|
return c.GetInt(string(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextKeyBool(c *gin.Context, key constant.ContextKey) bool {
|
||||||
|
return c.GetBool(string(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextKeyStringSlice(c *gin.Context, key constant.ContextKey) []string {
|
||||||
|
return c.GetStringSlice(string(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextKeyStringMap(c *gin.Context, key constant.ContextKey) map[string]any {
|
||||||
|
return c.GetStringMap(string(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextKeyTime(c *gin.Context, key constant.ContextKey) time.Time {
|
||||||
|
return c.GetTime(string(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextKeyType[T any](c *gin.Context, key constant.ContextKey) (T, bool) {
|
||||||
|
if value, ok := c.Get(string(key)); ok {
|
||||||
|
if v, ok := value.(T); ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var t T
|
||||||
|
return t, false
|
||||||
|
}
|
||||||
|
|||||||
24
common/gopool.go
Normal file
24
common/gopool.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bytedance/gopkg/util/gopool"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
var relayGoPool gopool.Pool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
relayGoPool = gopool.NewPool("gopool.RelayPool", math.MaxInt32, gopool.NewConfig())
|
||||||
|
relayGoPool.SetPanicHandler(func(ctx context.Context, i interface{}) {
|
||||||
|
if stopChan, ok := ctx.Value("stop_chan").(chan bool); ok {
|
||||||
|
SafeSendBool(stopChan, true)
|
||||||
|
}
|
||||||
|
SysError(fmt.Sprintf("panic in gopool.RelayPool: %v", i))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RelayCtxGo(ctx context.Context, f func()) {
|
||||||
|
relayGoPool.CtxGo(ctx, f)
|
||||||
|
}
|
||||||
57
common/http.go
Normal file
57
common/http.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CloseResponseBodyGracefully(httpResponse *http.Response) {
|
||||||
|
if httpResponse == nil || httpResponse.Body == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := httpResponse.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
SysError("failed to close response body: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IOCopyBytesGracefully(c *gin.Context, src *http.Response, data []byte) {
|
||||||
|
if c.Writer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := io.NopCloser(bytes.NewBuffer(data))
|
||||||
|
|
||||||
|
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
||||||
|
// And then we will have to send an error response, but in this case, the header has already been set.
|
||||||
|
// So the httpClient will be confused by the response.
|
||||||
|
// For example, Postman will report error, and we cannot check the response at all.
|
||||||
|
if src != nil {
|
||||||
|
for k, v := range src.Header {
|
||||||
|
// avoid setting Content-Length
|
||||||
|
if k == "Content-Length" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set(k, v[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set Content-Length header manually BEFORE calling WriteHeader
|
||||||
|
c.Writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
|
||||||
|
|
||||||
|
// Write header with status code (this sends the headers)
|
||||||
|
if src != nil {
|
||||||
|
c.Writer.WriteHeader(src.StatusCode)
|
||||||
|
} else {
|
||||||
|
c.Writer.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := io.Copy(c.Writer, body)
|
||||||
|
if err != nil {
|
||||||
|
LogError(c, fmt.Sprintf("failed to copy response body: %s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,11 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"one-api/constant"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -22,7 +25,7 @@ func printHelp() {
|
|||||||
fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]")
|
fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]")
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadEnv() {
|
func InitEnv() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *PrintVersion {
|
if *PrintVersion {
|
||||||
@@ -66,4 +69,52 @@ func LoadEnv() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize variables from constants.go that were using environment variables
|
||||||
|
DebugEnabled = os.Getenv("DEBUG") == "true"
|
||||||
|
MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true"
|
||||||
|
IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
||||||
|
|
||||||
|
// Parse requestInterval and set RequestInterval
|
||||||
|
requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
||||||
|
RequestInterval = time.Duration(requestInterval) * time.Second
|
||||||
|
|
||||||
|
// Initialize variables with GetEnvOrDefault
|
||||||
|
SyncFrequency = GetEnvOrDefault("SYNC_FREQUENCY", 60)
|
||||||
|
BatchUpdateInterval = GetEnvOrDefault("BATCH_UPDATE_INTERVAL", 5)
|
||||||
|
RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0)
|
||||||
|
|
||||||
|
// Initialize string variables with GetEnvOrDefaultString
|
||||||
|
GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE")
|
||||||
|
CohereSafetySetting = GetEnvOrDefaultString("COHERE_SAFETY_SETTING", "NONE")
|
||||||
|
|
||||||
|
// Initialize rate limit variables
|
||||||
|
GlobalApiRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_API_RATE_LIMIT_ENABLE", true)
|
||||||
|
GlobalApiRateLimitNum = GetEnvOrDefault("GLOBAL_API_RATE_LIMIT", 180)
|
||||||
|
GlobalApiRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_API_RATE_LIMIT_DURATION", 180))
|
||||||
|
|
||||||
|
GlobalWebRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_WEB_RATE_LIMIT_ENABLE", true)
|
||||||
|
GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 60)
|
||||||
|
GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180))
|
||||||
|
|
||||||
|
initConstantEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConstantEnv() {
|
||||||
|
constant.StreamingTimeout = GetEnvOrDefault("STREAMING_TIMEOUT", 120)
|
||||||
|
constant.DifyDebug = GetEnvOrDefaultBool("DIFY_DEBUG", true)
|
||||||
|
constant.MaxFileDownloadMB = GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20)
|
||||||
|
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
||||||
|
constant.ForceStreamOption = GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true)
|
||||||
|
constant.GetMediaToken = GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true)
|
||||||
|
constant.GetMediaTokenNotStream = GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true)
|
||||||
|
constant.UpdateTask = GetEnvOrDefaultBool("UPDATE_TASK", true)
|
||||||
|
constant.AzureDefaultAPIVersion = GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2025-04-01-preview")
|
||||||
|
constant.GeminiVisionMaxImageNum = GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
|
||||||
|
constant.NotifyLimitCount = GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2)
|
||||||
|
constant.NotificationLimitDurationMinute = GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
|
||||||
|
// GenerateDefaultToken 是否生成初始令牌,默认关闭。
|
||||||
|
constant.GenerateDefaultToken = GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)
|
||||||
|
// 是否启用错误日志
|
||||||
|
constant.ErrorLogEnabled = GetEnvOrDefaultBool("ERROR_LOG_ENABLED", false)
|
||||||
}
|
}
|
||||||
|
|||||||
22
common/json.go
Normal file
22
common/json.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UnmarshalJson(data []byte, v any) error {
|
||||||
|
return json.Unmarshal(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalJsonStr(data string, v any) error {
|
||||||
|
return json.Unmarshal(StringToByteSlice(data), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeJson(reader *bytes.Reader, v any) error {
|
||||||
|
return json.NewDecoder(reader).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeJson(v any) ([]byte, error) {
|
||||||
|
return json.Marshal(v)
|
||||||
|
}
|
||||||
89
common/limiter/limiter.go
Normal file
89
common/limiter/limiter.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package limiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"one-api/common"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed lua/rate_limit.lua
|
||||||
|
var rateLimitScript string
|
||||||
|
|
||||||
|
type RedisLimiter struct {
|
||||||
|
client *redis.Client
|
||||||
|
limitScriptSHA string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
instance *RedisLimiter
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(ctx context.Context, r *redis.Client) *RedisLimiter {
|
||||||
|
once.Do(func() {
|
||||||
|
// 预加载脚本
|
||||||
|
limitSHA, err := r.ScriptLoad(ctx, rateLimitScript).Result()
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(fmt.Sprintf("Failed to load rate limit script: %v", err))
|
||||||
|
}
|
||||||
|
instance = &RedisLimiter{
|
||||||
|
client: r,
|
||||||
|
limitScriptSHA: limitSHA,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RedisLimiter) Allow(ctx context.Context, key string, opts ...Option) (bool, error) {
|
||||||
|
// 默认配置
|
||||||
|
config := &Config{
|
||||||
|
Capacity: 10,
|
||||||
|
Rate: 1,
|
||||||
|
Requested: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用选项模式
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行限流
|
||||||
|
result, err := rl.client.EvalSha(
|
||||||
|
ctx,
|
||||||
|
rl.limitScriptSHA,
|
||||||
|
[]string{key},
|
||||||
|
config.Requested,
|
||||||
|
config.Rate,
|
||||||
|
config.Capacity,
|
||||||
|
).Int()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("rate limit failed: %w", err)
|
||||||
|
}
|
||||||
|
return result == 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config 配置选项模式
|
||||||
|
type Config struct {
|
||||||
|
Capacity int64
|
||||||
|
Rate int64
|
||||||
|
Requested int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*Config)
|
||||||
|
|
||||||
|
func WithCapacity(c int64) Option {
|
||||||
|
return func(cfg *Config) { cfg.Capacity = c }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRate(r int64) Option {
|
||||||
|
return func(cfg *Config) { cfg.Rate = r }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRequested(n int64) Option {
|
||||||
|
return func(cfg *Config) { cfg.Requested = n }
|
||||||
|
}
|
||||||
44
common/limiter/lua/rate_limit.lua
Normal file
44
common/limiter/lua/rate_limit.lua
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
-- 令牌桶限流器
|
||||||
|
-- KEYS[1]: 限流器唯一标识
|
||||||
|
-- ARGV[1]: 请求令牌数 (通常为1)
|
||||||
|
-- ARGV[2]: 令牌生成速率 (每秒)
|
||||||
|
-- ARGV[3]: 桶容量
|
||||||
|
|
||||||
|
local key = KEYS[1]
|
||||||
|
local requested = tonumber(ARGV[1])
|
||||||
|
local rate = tonumber(ARGV[2])
|
||||||
|
local capacity = tonumber(ARGV[3])
|
||||||
|
|
||||||
|
-- 获取当前时间(Redis服务器时间)
|
||||||
|
local now = redis.call('TIME')
|
||||||
|
local nowInSeconds = tonumber(now[1])
|
||||||
|
|
||||||
|
-- 获取桶状态
|
||||||
|
local bucket = redis.call('HMGET', key, 'tokens', 'last_time')
|
||||||
|
local tokens = tonumber(bucket[1])
|
||||||
|
local last_time = tonumber(bucket[2])
|
||||||
|
|
||||||
|
-- 初始化桶(首次请求或过期)
|
||||||
|
if not tokens or not last_time then
|
||||||
|
tokens = capacity
|
||||||
|
last_time = nowInSeconds
|
||||||
|
else
|
||||||
|
-- 计算新增令牌
|
||||||
|
local elapsed = nowInSeconds - last_time
|
||||||
|
local add_tokens = elapsed * rate
|
||||||
|
tokens = math.min(capacity, tokens + add_tokens)
|
||||||
|
last_time = nowInSeconds
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 判断是否允许请求
|
||||||
|
local allowed = false
|
||||||
|
if tokens >= requested then
|
||||||
|
tokens = tokens - requested
|
||||||
|
allowed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
---- 更新桶状态并设置过期时间
|
||||||
|
redis.call('HMSET', key, 'tokens', tokens, 'last_time', last_time)
|
||||||
|
--redis.call('EXPIRE', key, math.ceil(capacity / rate) + 60) -- 适当延长过期时间
|
||||||
|
|
||||||
|
return allowed and 1 or 0
|
||||||
@@ -1,493 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// from songquanpeng/one-api
|
|
||||||
const (
|
|
||||||
USD2RMB = 7.3 // 暂定 1 USD = 7.3 RMB
|
|
||||||
USD = 500 // $0.002 = 1 -> $1 = 500
|
|
||||||
RMB = USD / USD2RMB
|
|
||||||
)
|
|
||||||
|
|
||||||
// modelRatio
|
|
||||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
|
||||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf
|
|
||||||
// https://openai.com/pricing
|
|
||||||
// TODO: when a new api is enabled, check the pricing here
|
|
||||||
// 1 === $0.002 / 1K tokens
|
|
||||||
// 1 === ¥0.014 / 1k tokens
|
|
||||||
|
|
||||||
var defaultModelRatio = map[string]float64{
|
|
||||||
//"midjourney": 50,
|
|
||||||
"gpt-4-gizmo-*": 15,
|
|
||||||
"gpt-4o-gizmo-*": 2.5,
|
|
||||||
"gpt-4-all": 15,
|
|
||||||
"gpt-4o-all": 15,
|
|
||||||
"gpt-4": 15,
|
|
||||||
//"gpt-4-0314": 15, //deprecated
|
|
||||||
"gpt-4-0613": 15,
|
|
||||||
"gpt-4-32k": 30,
|
|
||||||
//"gpt-4-32k-0314": 30, //deprecated
|
|
||||||
"gpt-4-32k-0613": 30,
|
|
||||||
"gpt-4-1106-preview": 5, // $10 / 1M tokens
|
|
||||||
"gpt-4-0125-preview": 5, // $10 / 1M tokens
|
|
||||||
"gpt-4-turbo-preview": 5, // $10 / 1M tokens
|
|
||||||
"gpt-4-vision-preview": 5, // $10 / 1M tokens
|
|
||||||
"gpt-4-1106-vision-preview": 5, // $10 / 1M tokens
|
|
||||||
"chatgpt-4o-latest": 2.5, // $5 / 1M tokens
|
|
||||||
"gpt-4o": 1.25, // $2.5 / 1M tokens
|
|
||||||
"gpt-4o-audio-preview": 1.25, // $2.5 / 1M tokens
|
|
||||||
"gpt-4o-audio-preview-2024-10-01": 1.25, // $2.5 / 1M tokens
|
|
||||||
"gpt-4o-2024-05-13": 2.5, // $5 / 1M tokens
|
|
||||||
"gpt-4o-2024-08-06": 1.25, // $2.5 / 1M tokens
|
|
||||||
"gpt-4o-2024-11-20": 1.25, // $2.5 / 1M tokens
|
|
||||||
"gpt-4o-realtime-preview": 2.5,
|
|
||||||
"gpt-4o-realtime-preview-2024-10-01": 2.5,
|
|
||||||
"gpt-4o-realtime-preview-2024-12-17": 2.5,
|
|
||||||
"gpt-4o-mini-realtime-preview": 0.3,
|
|
||||||
"gpt-4o-mini-realtime-preview-2024-12-17": 0.3,
|
|
||||||
"o1": 7.5,
|
|
||||||
"o1-2024-12-17": 7.5,
|
|
||||||
"o1-preview": 7.5,
|
|
||||||
"o1-preview-2024-09-12": 7.5,
|
|
||||||
"o1-mini": 0.55,
|
|
||||||
"o1-mini-2024-09-12": 0.55,
|
|
||||||
"o3-mini": 0.55,
|
|
||||||
"o3-mini-2025-01-31": 0.55,
|
|
||||||
"o3-mini-high": 0.55,
|
|
||||||
"o3-mini-2025-01-31-high": 0.55,
|
|
||||||
"o3-mini-low": 0.55,
|
|
||||||
"o3-mini-2025-01-31-low": 0.55,
|
|
||||||
"o3-mini-medium": 0.55,
|
|
||||||
"o3-mini-2025-01-31-medium": 0.55,
|
|
||||||
"gpt-4o-mini": 0.075,
|
|
||||||
"gpt-4o-mini-2024-07-18": 0.075,
|
|
||||||
"gpt-4-turbo": 5, // $0.01 / 1K tokens
|
|
||||||
"gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
|
|
||||||
//"gpt-3.5-turbo-0301": 0.75, //deprecated
|
|
||||||
"gpt-3.5-turbo": 0.25,
|
|
||||||
"gpt-3.5-turbo-0613": 0.75,
|
|
||||||
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
|
|
||||||
"gpt-3.5-turbo-16k-0613": 1.5,
|
|
||||||
"gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens
|
|
||||||
"gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens
|
|
||||||
"gpt-3.5-turbo-0125": 0.25,
|
|
||||||
"babbage-002": 0.2, // $0.0004 / 1K tokens
|
|
||||||
"davinci-002": 1, // $0.002 / 1K tokens
|
|
||||||
"text-ada-001": 0.2,
|
|
||||||
"text-babbage-001": 0.25,
|
|
||||||
"text-curie-001": 1,
|
|
||||||
//"text-davinci-002": 10,
|
|
||||||
//"text-davinci-003": 10,
|
|
||||||
"text-davinci-edit-001": 10,
|
|
||||||
"code-davinci-edit-001": 10,
|
|
||||||
"whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens
|
|
||||||
"tts-1": 7.5, // 1k characters -> $0.015
|
|
||||||
"tts-1-1106": 7.5, // 1k characters -> $0.015
|
|
||||||
"tts-1-hd": 15, // 1k characters -> $0.03
|
|
||||||
"tts-1-hd-1106": 15, // 1k characters -> $0.03
|
|
||||||
"davinci": 10,
|
|
||||||
"curie": 10,
|
|
||||||
"babbage": 10,
|
|
||||||
"ada": 10,
|
|
||||||
"text-embedding-3-small": 0.01,
|
|
||||||
"text-embedding-3-large": 0.065,
|
|
||||||
"text-embedding-ada-002": 0.05,
|
|
||||||
"text-search-ada-doc-001": 10,
|
|
||||||
"text-moderation-stable": 0.1,
|
|
||||||
"text-moderation-latest": 0.1,
|
|
||||||
"claude-instant-1": 0.4, // $0.8 / 1M tokens
|
|
||||||
"claude-2.0": 4, // $8 / 1M tokens
|
|
||||||
"claude-2.1": 4, // $8 / 1M tokens
|
|
||||||
"claude-3-haiku-20240307": 0.125, // $0.25 / 1M tokens
|
|
||||||
"claude-3-5-haiku-20241022": 0.5, // $1 / 1M tokens
|
|
||||||
"claude-3-sonnet-20240229": 1.5, // $3 / 1M tokens
|
|
||||||
"claude-3-5-sonnet-20240620": 1.5,
|
|
||||||
"claude-3-5-sonnet-20241022": 1.5,
|
|
||||||
"claude-3-opus-20240229": 7.5, // $15 / 1M tokens
|
|
||||||
"ERNIE-4.0-8K": 0.120 * RMB,
|
|
||||||
"ERNIE-3.5-8K": 0.012 * RMB,
|
|
||||||
"ERNIE-3.5-8K-0205": 0.024 * RMB,
|
|
||||||
"ERNIE-3.5-8K-1222": 0.012 * RMB,
|
|
||||||
"ERNIE-Bot-8K": 0.024 * RMB,
|
|
||||||
"ERNIE-3.5-4K-0205": 0.012 * RMB,
|
|
||||||
"ERNIE-Speed-8K": 0.004 * RMB,
|
|
||||||
"ERNIE-Speed-128K": 0.004 * RMB,
|
|
||||||
"ERNIE-Lite-8K-0922": 0.008 * RMB,
|
|
||||||
"ERNIE-Lite-8K-0308": 0.003 * RMB,
|
|
||||||
"ERNIE-Tiny-8K": 0.001 * RMB,
|
|
||||||
"BLOOMZ-7B": 0.004 * RMB,
|
|
||||||
"Embedding-V1": 0.002 * RMB,
|
|
||||||
"bge-large-zh": 0.002 * RMB,
|
|
||||||
"bge-large-en": 0.002 * RMB,
|
|
||||||
"tao-8k": 0.002 * RMB,
|
|
||||||
"PaLM-2": 1,
|
|
||||||
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
|
||||||
"gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
|
||||||
"gemini-1.0-pro-vision-001": 1,
|
|
||||||
"gemini-1.0-pro-001": 1,
|
|
||||||
"gemini-1.5-pro-latest": 1.75, // $3.5 / 1M tokens
|
|
||||||
"gemini-1.5-pro-exp-0827": 1.75, // $3.5 / 1M tokens
|
|
||||||
"gemini-1.5-flash-latest": 1,
|
|
||||||
"gemini-1.5-flash-exp-0827": 1,
|
|
||||||
"gemini-1.0-pro-latest": 1,
|
|
||||||
"gemini-1.0-pro-vision-latest": 1,
|
|
||||||
"gemini-ultra": 1,
|
|
||||||
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
|
|
||||||
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
|
||||||
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
|
|
||||||
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
|
|
||||||
"glm-4": 7.143, // ¥0.1 / 1k tokens
|
|
||||||
"glm-4v": 0.05 * RMB, // ¥0.05 / 1k tokens
|
|
||||||
"glm-4-alltools": 0.1 * RMB, // ¥0.1 / 1k tokens
|
|
||||||
"glm-3-turbo": 0.3572,
|
|
||||||
"glm-4-plus": 0.05 * RMB,
|
|
||||||
"glm-4-0520": 0.1 * RMB,
|
|
||||||
"glm-4-air": 0.001 * RMB,
|
|
||||||
"glm-4-airx": 0.01 * RMB,
|
|
||||||
"glm-4-long": 0.001 * RMB,
|
|
||||||
"glm-4-flash": 0,
|
|
||||||
"glm-4v-plus": 0.01 * RMB,
|
|
||||||
"qwen-turbo": 0.8572, // ¥0.012 / 1k tokens
|
|
||||||
"qwen-plus": 10, // ¥0.14 / 1k tokens
|
|
||||||
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
|
|
||||||
"SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens
|
|
||||||
"SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens
|
|
||||||
"SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens
|
|
||||||
"SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens
|
|
||||||
"SparkDesk-v4.0": 1.2858,
|
|
||||||
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
|
|
||||||
"360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens
|
|
||||||
"360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens
|
|
||||||
"360gpt-pro": 0.8572, // ¥0.012 / 1k tokens
|
|
||||||
"360gpt2-pro": 0.8572, // ¥0.012 / 1k tokens
|
|
||||||
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
|
|
||||||
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
|
||||||
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
|
||||||
"hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
|
|
||||||
// https://platform.lingyiwanwu.com/docs#-计费单元
|
|
||||||
// 已经按照 7.2 来换算美元价格
|
|
||||||
"yi-34b-chat-0205": 0.18,
|
|
||||||
"yi-34b-chat-200k": 0.864,
|
|
||||||
"yi-vl-plus": 0.432,
|
|
||||||
"yi-large": 20.0 / 1000 * RMB,
|
|
||||||
"yi-medium": 2.5 / 1000 * RMB,
|
|
||||||
"yi-vision": 6.0 / 1000 * RMB,
|
|
||||||
"yi-medium-200k": 12.0 / 1000 * RMB,
|
|
||||||
"yi-spark": 1.0 / 1000 * RMB,
|
|
||||||
"yi-large-rag": 25.0 / 1000 * RMB,
|
|
||||||
"yi-large-turbo": 12.0 / 1000 * RMB,
|
|
||||||
"yi-large-preview": 20.0 / 1000 * RMB,
|
|
||||||
"yi-large-rag-preview": 25.0 / 1000 * RMB,
|
|
||||||
"command": 0.5,
|
|
||||||
"command-nightly": 0.5,
|
|
||||||
"command-light": 0.5,
|
|
||||||
"command-light-nightly": 0.5,
|
|
||||||
"command-r": 0.25,
|
|
||||||
"command-r-plus": 1.5,
|
|
||||||
"command-r-08-2024": 0.075,
|
|
||||||
"command-r-plus-08-2024": 1.25,
|
|
||||||
"deepseek-chat": 0.27 / 2,
|
|
||||||
"deepseek-coder": 0.27 / 2,
|
|
||||||
"deepseek-reasoner": 0.55 / 2, // 0.55 / 1k tokens
|
|
||||||
// Perplexity online 模型对搜索额外收费,有需要应自行调整,此处不计入搜索费用
|
|
||||||
"llama-3-sonar-small-32k-chat": 0.2 / 1000 * USD,
|
|
||||||
"llama-3-sonar-small-32k-online": 0.2 / 1000 * USD,
|
|
||||||
"llama-3-sonar-large-32k-chat": 1 / 1000 * USD,
|
|
||||||
"llama-3-sonar-large-32k-online": 1 / 1000 * USD,
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultModelPrice = map[string]float64{
|
|
||||||
"suno_music": 0.1,
|
|
||||||
"suno_lyrics": 0.01,
|
|
||||||
"dall-e-3": 0.04,
|
|
||||||
"gpt-4-gizmo-*": 0.1,
|
|
||||||
"mj_imagine": 0.1,
|
|
||||||
"mj_variation": 0.1,
|
|
||||||
"mj_reroll": 0.1,
|
|
||||||
"mj_blend": 0.1,
|
|
||||||
"mj_modal": 0.1,
|
|
||||||
"mj_zoom": 0.1,
|
|
||||||
"mj_shorten": 0.1,
|
|
||||||
"mj_high_variation": 0.1,
|
|
||||||
"mj_low_variation": 0.1,
|
|
||||||
"mj_pan": 0.1,
|
|
||||||
"mj_inpaint": 0,
|
|
||||||
"mj_custom_zoom": 0,
|
|
||||||
"mj_describe": 0.05,
|
|
||||||
"mj_upscale": 0.05,
|
|
||||||
"swap_face": 0.05,
|
|
||||||
"mj_upload": 0.05,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
modelPriceMap map[string]float64 = nil
|
|
||||||
modelPriceMapMutex = sync.RWMutex{}
|
|
||||||
)
|
|
||||||
var (
|
|
||||||
modelRatioMap map[string]float64 = nil
|
|
||||||
modelRatioMapMutex = sync.RWMutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
CompletionRatio map[string]float64 = nil
|
|
||||||
CompletionRatioMutex = sync.RWMutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultCompletionRatio = map[string]float64{
|
|
||||||
"gpt-4-gizmo-*": 2,
|
|
||||||
"gpt-4o-gizmo-*": 3,
|
|
||||||
"gpt-4-all": 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetModelPriceMap() map[string]float64 {
|
|
||||||
modelPriceMapMutex.Lock()
|
|
||||||
defer modelPriceMapMutex.Unlock()
|
|
||||||
if modelPriceMap == nil {
|
|
||||||
modelPriceMap = defaultModelPrice
|
|
||||||
}
|
|
||||||
return modelPriceMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func ModelPrice2JSONString() string {
|
|
||||||
GetModelPriceMap()
|
|
||||||
jsonBytes, err := json.Marshal(modelPriceMap)
|
|
||||||
if err != nil {
|
|
||||||
SysError("error marshalling model price: " + err.Error())
|
|
||||||
}
|
|
||||||
return string(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateModelPriceByJSONString(jsonStr string) error {
|
|
||||||
modelPriceMapMutex.Lock()
|
|
||||||
defer modelPriceMapMutex.Unlock()
|
|
||||||
modelPriceMap = make(map[string]float64)
|
|
||||||
return json.Unmarshal([]byte(jsonStr), &modelPriceMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetModelPrice 返回模型的价格,如果模型不存在则返回-1,false
|
|
||||||
func GetModelPrice(name string, printErr bool) (float64, bool) {
|
|
||||||
GetModelPriceMap()
|
|
||||||
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
|
||||||
name = "gpt-4-gizmo-*"
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "gpt-4o-gizmo") {
|
|
||||||
name = "gpt-4o-gizmo-*"
|
|
||||||
}
|
|
||||||
price, ok := modelPriceMap[name]
|
|
||||||
if !ok {
|
|
||||||
if printErr {
|
|
||||||
SysError("model price not found: " + name)
|
|
||||||
}
|
|
||||||
return -1, false
|
|
||||||
}
|
|
||||||
return price, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetModelRatioMap() map[string]float64 {
|
|
||||||
modelRatioMapMutex.Lock()
|
|
||||||
defer modelRatioMapMutex.Unlock()
|
|
||||||
if modelRatioMap == nil {
|
|
||||||
modelRatioMap = defaultModelRatio
|
|
||||||
}
|
|
||||||
return modelRatioMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func ModelRatio2JSONString() string {
|
|
||||||
GetModelRatioMap()
|
|
||||||
jsonBytes, err := json.Marshal(modelRatioMap)
|
|
||||||
if err != nil {
|
|
||||||
SysError("error marshalling model ratio: " + err.Error())
|
|
||||||
}
|
|
||||||
return string(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateModelRatioByJSONString(jsonStr string) error {
|
|
||||||
modelRatioMapMutex.Lock()
|
|
||||||
defer modelRatioMapMutex.Unlock()
|
|
||||||
modelRatioMap = make(map[string]float64)
|
|
||||||
return json.Unmarshal([]byte(jsonStr), &modelRatioMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetModelRatio(name string) float64 {
|
|
||||||
GetModelRatioMap()
|
|
||||||
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
|
||||||
name = "gpt-4-gizmo-*"
|
|
||||||
}
|
|
||||||
ratio, ok := modelRatioMap[name]
|
|
||||||
if !ok {
|
|
||||||
SysError("model ratio not found: " + name)
|
|
||||||
return 30
|
|
||||||
}
|
|
||||||
return ratio
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultModelRatio2JSONString() string {
|
|
||||||
jsonBytes, err := json.Marshal(defaultModelRatio)
|
|
||||||
if err != nil {
|
|
||||||
SysError("error marshalling model ratio: " + err.Error())
|
|
||||||
}
|
|
||||||
return string(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDefaultModelRatioMap() map[string]float64 {
|
|
||||||
return defaultModelRatio
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCompletionRatioMap() map[string]float64 {
|
|
||||||
CompletionRatioMutex.Lock()
|
|
||||||
defer CompletionRatioMutex.Unlock()
|
|
||||||
if CompletionRatio == nil {
|
|
||||||
CompletionRatio = defaultCompletionRatio
|
|
||||||
}
|
|
||||||
return CompletionRatio
|
|
||||||
}
|
|
||||||
|
|
||||||
func CompletionRatio2JSONString() string {
|
|
||||||
GetCompletionRatioMap()
|
|
||||||
jsonBytes, err := json.Marshal(CompletionRatio)
|
|
||||||
if err != nil {
|
|
||||||
SysError("error marshalling completion ratio: " + err.Error())
|
|
||||||
}
|
|
||||||
return string(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateCompletionRatioByJSONString(jsonStr string) error {
|
|
||||||
CompletionRatioMutex.Lock()
|
|
||||||
defer CompletionRatioMutex.Unlock()
|
|
||||||
CompletionRatio = make(map[string]float64)
|
|
||||||
return json.Unmarshal([]byte(jsonStr), &CompletionRatio)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCompletionRatio(name string) float64 {
|
|
||||||
GetCompletionRatioMap()
|
|
||||||
|
|
||||||
if strings.Contains(name, "/") {
|
|
||||||
if ratio, ok := CompletionRatio[name]; ok {
|
|
||||||
return ratio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lowercaseName := strings.ToLower(name)
|
|
||||||
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
|
||||||
name = "gpt-4-gizmo-*"
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "gpt-4o-gizmo") {
|
|
||||||
name = "gpt-4o-gizmo-*"
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
|
|
||||||
if strings.HasPrefix(name, "gpt-4o") {
|
|
||||||
if name == "gpt-4o-2024-05-13" {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "preview") {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "o1") || strings.HasPrefix(name, "o3") {
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
if name == "chatgpt-4o-latest" {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
if strings.Contains(name, "claude-instant-1") {
|
|
||||||
return 3
|
|
||||||
} else if strings.Contains(name, "claude-2") {
|
|
||||||
return 3
|
|
||||||
} else if strings.Contains(name, "claude-3") {
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "gpt-3.5") {
|
|
||||||
if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
|
|
||||||
// https://openai.com/blog/new-embedding-models-and-api-updates
|
|
||||||
// Updated GPT-3.5 Turbo model and lower pricing
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(name, "1106") {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
return 4.0 / 3.0
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "mistral-") {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "gemini-") {
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "command") {
|
|
||||||
switch name {
|
|
||||||
case "command-r":
|
|
||||||
return 3
|
|
||||||
case "command-r-plus":
|
|
||||||
return 5
|
|
||||||
case "command-r-08-2024":
|
|
||||||
return 4
|
|
||||||
case "command-r-plus-08-2024":
|
|
||||||
return 4
|
|
||||||
default:
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐
|
|
||||||
if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" {
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "ERNIE-Speed-") {
|
|
||||||
return 2
|
|
||||||
} else if strings.HasPrefix(name, "ERNIE-Lite-") {
|
|
||||||
return 2
|
|
||||||
} else if strings.HasPrefix(name, "ERNIE-Character") {
|
|
||||||
return 2
|
|
||||||
} else if strings.HasPrefix(name, "ERNIE-Functions") {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
switch name {
|
|
||||||
case "llama2-70b-4096":
|
|
||||||
return 0.8 / 0.64
|
|
||||||
case "llama3-8b-8192":
|
|
||||||
return 2
|
|
||||||
case "llama3-70b-8192":
|
|
||||||
return 0.79 / 0.59
|
|
||||||
}
|
|
||||||
if ratio, ok := CompletionRatio[name]; ok {
|
|
||||||
return ratio
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAudioRatio(name string) float64 {
|
|
||||||
if strings.Contains(name, "-realtime") {
|
|
||||||
if strings.HasSuffix(name, "gpt-4o-realtime-preview-2024-12-17") {
|
|
||||||
return 8
|
|
||||||
} else if strings.Contains(name, "mini") {
|
|
||||||
return 10 / 0.6
|
|
||||||
} else {
|
|
||||||
return 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Contains(name, "-audio") {
|
|
||||||
if strings.HasSuffix(name, "gpt-4o-audio-preview-2024-12-17") {
|
|
||||||
return 16
|
|
||||||
} else if strings.Contains(name, "mini") {
|
|
||||||
return 10 / 0.15
|
|
||||||
} else {
|
|
||||||
return 40
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 20
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAudioCompletionRatio(name string) float64 {
|
|
||||||
if strings.HasPrefix(name, "gpt-4o-realtime") {
|
|
||||||
return 2
|
|
||||||
} else if strings.HasPrefix(name, "gpt-4o-mini-realtime") {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
42
common/model.go
Normal file
42
common/model.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// OpenAIResponseOnlyModels is a list of models that are only available for OpenAI responses.
|
||||||
|
OpenAIResponseOnlyModels = []string{
|
||||||
|
"o3-pro",
|
||||||
|
"o3-deep-research",
|
||||||
|
"o4-mini-deep-research",
|
||||||
|
}
|
||||||
|
ImageGenerationModels = []string{
|
||||||
|
"dall-e-3",
|
||||||
|
"dall-e-2",
|
||||||
|
"gpt-image-1",
|
||||||
|
"prefix:imagen-",
|
||||||
|
"flux-",
|
||||||
|
"flux.1-",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsOpenAIResponseOnlyModel(modelName string) bool {
|
||||||
|
for _, m := range OpenAIResponseOnlyModels {
|
||||||
|
if strings.Contains(modelName, m) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsImageGenerationModel(modelName string) bool {
|
||||||
|
modelName = strings.ToLower(modelName)
|
||||||
|
for _, m := range ImageGenerationModels {
|
||||||
|
if strings.Contains(modelName, m) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(m, "prefix:") && strings.HasPrefix(modelName, strings.TrimPrefix(m, "prefix:")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
62
common/page_info.go
Normal file
62
common/page_info.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PageInfo struct {
|
||||||
|
Page int `json:"page"` // page num 页码
|
||||||
|
PageSize int `json:"page_size"` // page size 页大小
|
||||||
|
StartTimestamp int64 `json:"start_timestamp"` // 秒级
|
||||||
|
EndTimestamp int64 `json:"end_timestamp"` // 秒级
|
||||||
|
|
||||||
|
Total int `json:"total"` // 总条数,后设置
|
||||||
|
Items any `json:"items"` // 数据,后设置
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PageInfo) GetStartIdx() int {
|
||||||
|
return (p.Page - 1) * p.PageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PageInfo) GetEndIdx() int {
|
||||||
|
return p.Page * p.PageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PageInfo) GetPageSize() int {
|
||||||
|
return p.PageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PageInfo) GetPage() int {
|
||||||
|
return p.Page
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PageInfo) SetTotal(total int) {
|
||||||
|
p.Total = total
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PageInfo) SetItems(items any) {
|
||||||
|
p.Items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPageQuery(c *gin.Context) (*PageInfo, error) {
|
||||||
|
pageInfo := &PageInfo{}
|
||||||
|
err := c.BindQuery(pageInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if pageInfo.Page < 1 {
|
||||||
|
// 兼容
|
||||||
|
page, _ := strconv.Atoi(c.Query("p"))
|
||||||
|
if page != 0 {
|
||||||
|
pageInfo.Page = page
|
||||||
|
} else {
|
||||||
|
pageInfo.Page = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageInfo.PageSize == 0 {
|
||||||
|
pageInfo.PageSize = ItemsPerPage
|
||||||
|
}
|
||||||
|
return pageInfo, nil
|
||||||
|
}
|
||||||
@@ -16,6 +16,10 @@ import (
|
|||||||
var RDB *redis.Client
|
var RDB *redis.Client
|
||||||
var RedisEnabled = true
|
var RedisEnabled = true
|
||||||
|
|
||||||
|
func RedisKeyCacheSeconds() int {
|
||||||
|
return SyncFrequency
|
||||||
|
}
|
||||||
|
|
||||||
// InitRedisClient This function is called after init()
|
// InitRedisClient This function is called after init()
|
||||||
func InitRedisClient() (err error) {
|
func InitRedisClient() (err error) {
|
||||||
if os.Getenv("REDIS_CONN_STRING") == "" {
|
if os.Getenv("REDIS_CONN_STRING") == "" {
|
||||||
@@ -32,6 +36,7 @@ func InitRedisClient() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
FatalLog("failed to parse Redis connection string: " + err.Error())
|
FatalLog("failed to parse Redis connection string: " + err.Error())
|
||||||
}
|
}
|
||||||
|
opt.PoolSize = GetEnvOrDefault("REDIS_POOL_SIZE", 10)
|
||||||
RDB = redis.NewClient(opt)
|
RDB = redis.NewClient(opt)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
@@ -41,6 +46,10 @@ func InitRedisClient() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
FatalLog("Redis ping test failed: " + err.Error())
|
FatalLog("Redis ping test failed: " + err.Error())
|
||||||
}
|
}
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis connected to %s", opt.Addr))
|
||||||
|
SysLog(fmt.Sprintf("Redis database: %d", opt.DB))
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +62,20 @@ func ParseRedisOption() *redis.Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RedisSet(key string, value string, expiration time.Duration) error {
|
func RedisSet(key string, value string, expiration time.Duration) error {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis SET: key=%s, value=%s, expiration=%v", key, value, expiration))
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
return RDB.Set(ctx, key, value, expiration).Err()
|
return RDB.Set(ctx, key, value, expiration).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func RedisGet(key string) (string, error) {
|
func RedisGet(key string) (string, error) {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis GET: key=%s", key))
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
return RDB.Get(ctx, key).Result()
|
val, err := RDB.Get(ctx, key).Result()
|
||||||
|
return val, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//func RedisExpire(key string, expiration time.Duration) error {
|
//func RedisExpire(key string, expiration time.Duration) error {
|
||||||
@@ -73,16 +89,25 @@ func RedisGet(key string) (string, error) {
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
func RedisDel(key string) error {
|
func RedisDel(key string) error {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis DEL: key=%s", key))
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
return RDB.Del(ctx, key).Err()
|
return RDB.Del(ctx, key).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func RedisHDelObj(key string) error {
|
func RedisDelKey(key string) error {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis DEL Key: key=%s", key))
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
return RDB.HDel(ctx, key).Err()
|
return RDB.Del(ctx, key).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error {
|
func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis HSET: key=%s, obj=%+v, expiration=%v", key, obj, expiration))
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
@@ -120,7 +145,11 @@ func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error {
|
|||||||
|
|
||||||
txn := RDB.TxPipeline()
|
txn := RDB.TxPipeline()
|
||||||
txn.HSet(ctx, key, data)
|
txn.HSet(ctx, key, data)
|
||||||
txn.Expire(ctx, key, expiration)
|
|
||||||
|
// 只有在 expiration 大于 0 时才设置过期时间
|
||||||
|
if expiration > 0 {
|
||||||
|
txn.Expire(ctx, key, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
_, err := txn.Exec(ctx)
|
_, err := txn.Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -130,6 +159,9 @@ func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RedisHGetObj(key string, obj interface{}) error {
|
func RedisHGetObj(key string, obj interface{}) error {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis HGETALL: key=%s", key))
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
result, err := RDB.HGetAll(ctx, key).Result()
|
result, err := RDB.HGetAll(ctx, key).Result()
|
||||||
@@ -208,6 +240,9 @@ func RedisHGetObj(key string, obj interface{}) error {
|
|||||||
|
|
||||||
// RedisIncr Add this function to handle atomic increments
|
// RedisIncr Add this function to handle atomic increments
|
||||||
func RedisIncr(key string, delta int64) error {
|
func RedisIncr(key string, delta int64) error {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis INCR: key=%s, delta=%d", key, delta))
|
||||||
|
}
|
||||||
// 检查键的剩余生存时间
|
// 检查键的剩余生存时间
|
||||||
ttlCmd := RDB.TTL(context.Background(), key)
|
ttlCmd := RDB.TTL(context.Background(), key)
|
||||||
ttl, err := ttlCmd.Result()
|
ttl, err := ttlCmd.Result()
|
||||||
@@ -238,6 +273,9 @@ func RedisIncr(key string, delta int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RedisHIncrBy(key, field string, delta int64) error {
|
func RedisHIncrBy(key, field string, delta int64) error {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis HINCRBY: key=%s, field=%s, delta=%d", key, field, delta))
|
||||||
|
}
|
||||||
ttlCmd := RDB.TTL(context.Background(), key)
|
ttlCmd := RDB.TTL(context.Background(), key)
|
||||||
ttl, err := ttlCmd.Result()
|
ttl, err := ttlCmd.Result()
|
||||||
if err != nil && !errors.Is(err, redis.Nil) {
|
if err != nil && !errors.Is(err, redis.Nil) {
|
||||||
@@ -262,6 +300,9 @@ func RedisHIncrBy(key, field string, delta int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RedisHSetField(key, field string, value interface{}) error {
|
func RedisHSetField(key, field string, value interface{}) error {
|
||||||
|
if DebugEnabled {
|
||||||
|
SysLog(fmt.Sprintf("Redis HSET field: key=%s, field=%s, value=%v", key, field, value))
|
||||||
|
}
|
||||||
ttlCmd := RDB.TTL(context.Background(), key)
|
ttlCmd := RDB.TTL(context.Background(), key)
|
||||||
ttl, err := ttlCmd.Result()
|
ttl, err := ttlCmd.Result()
|
||||||
if err != nil && !errors.Is(err, redis.Nil) {
|
if err != nil && !errors.Is(err, redis.Nil) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -68,3 +69,15 @@ func StringToByteSlice(s string) []byte {
|
|||||||
tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]}
|
tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]}
|
||||||
return *(*[]byte)(unsafe.Pointer(&tmp2))
|
return *(*[]byte)(unsafe.Pointer(&tmp2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EncodeBase64(str string) string {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetJsonString(data any) string {
|
||||||
|
if data == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(data)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|||||||
149
common/struct_reflect.go
Normal file
149
common/struct_reflect.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StructToMap 递归地把任意结构体 v 转成 map[string]any。
|
||||||
|
// - 只处理导出字段;未导出字段会被跳过。
|
||||||
|
// - 优先使用 `json:"name"` 里逗号前的部分作为键;如果是 "-" 则忽略该字段;若无 tag,则使用字段名。
|
||||||
|
// - 对指针、切片、数组、嵌套结构体、map 做深度遍历,保持原始结构。
|
||||||
|
func StructToMap(v any) (map[string]any, error) {
|
||||||
|
val := reflect.ValueOf(v)
|
||||||
|
if !val.IsValid() {
|
||||||
|
return nil, fmt.Errorf("nil value")
|
||||||
|
}
|
||||||
|
for val.Kind() == reflect.Pointer {
|
||||||
|
if val.IsNil() {
|
||||||
|
return nil, fmt.Errorf("nil pointer")
|
||||||
|
}
|
||||||
|
val = val.Elem()
|
||||||
|
}
|
||||||
|
if val.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("expect struct, got %s", val.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
return structValueToMap(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func structValueToMap(val reflect.Value) map[string]any {
|
||||||
|
out := make(map[string]any, val.NumField())
|
||||||
|
|
||||||
|
typ := val.Type()
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
f := typ.Field(i)
|
||||||
|
if f.PkgPath != "" { // 未导出字段
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 json tag
|
||||||
|
tag := f.Tag.Get("json")
|
||||||
|
name, opts := parseTag(tag)
|
||||||
|
if name == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
fv := val.Field(i)
|
||||||
|
out[name] = valueToAny(fv, opts.Contains("omitempty"))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// valueToAny 递归处理各种值类型。
|
||||||
|
func valueToAny(v reflect.Value, omitEmpty bool) any {
|
||||||
|
if !v.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for v.Kind() == reflect.Pointer {
|
||||||
|
if v.IsNil() {
|
||||||
|
if omitEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 保持与 encoding/json 行为一致,nil 指针写成 null
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
return structValueToMap(v)
|
||||||
|
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
l := v.Len()
|
||||||
|
arr := make([]any, l)
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
arr[i] = valueToAny(v.Index(i), false)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
m := make(map[string]any, v.Len())
|
||||||
|
iter := v.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
k := iter.Key()
|
||||||
|
// 只支持 string key,与 encoding/json 一致
|
||||||
|
if k.Kind() == reflect.String {
|
||||||
|
m[k.String()] = valueToAny(iter.Value(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 基本类型直接返回其接口值
|
||||||
|
return v.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagOptions 用于判断是否包含 "omitempty"
|
||||||
|
type tagOptions string
|
||||||
|
|
||||||
|
func (o tagOptions) Contains(opt string) bool {
|
||||||
|
if len(o) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, s := range splitComma(string(o)) {
|
||||||
|
if s == opt {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTag(tag string) (string, tagOptions) {
|
||||||
|
if idx := indexComma(tag); idx != -1 {
|
||||||
|
return tag[:idx], tagOptions(tag[idx+1:])
|
||||||
|
}
|
||||||
|
return tag, tagOptions("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 避免 strings.Split 额外分配
|
||||||
|
func indexComma(s string) int {
|
||||||
|
for i, r := range s {
|
||||||
|
if r == ',' {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitComma(s string) []string {
|
||||||
|
var parts []string
|
||||||
|
start := 0
|
||||||
|
for i, r := range s {
|
||||||
|
if r == ',' {
|
||||||
|
parts = append(parts, s[start:i])
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start <= len(s) {
|
||||||
|
parts = append(parts, s[start:])
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
@@ -5,14 +5,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -21,6 +22,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func OpenBrowser(url string) {
|
func OpenBrowser(url string) {
|
||||||
@@ -213,6 +215,24 @@ func RandomSleep() {
|
|||||||
time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)
|
time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetPointer[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func Any2Type[T any](data any) (T, error) {
|
||||||
|
var zero T
|
||||||
|
bytes, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
var res T
|
||||||
|
err = json.Unmarshal(bytes, &res)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SaveTmpFile saves data to a temporary file. The filename would be apppended with a random string.
|
// SaveTmpFile saves data to a temporary file. The filename would be apppended with a random string.
|
||||||
func SaveTmpFile(filename string, data io.Reader) (string, error) {
|
func SaveTmpFile(filename string, data io.Reader) (string, error) {
|
||||||
f, err := os.CreateTemp(os.TempDir(), filename)
|
f, err := os.CreateTemp(os.TempDir(), filename)
|
||||||
@@ -230,13 +250,55 @@ func SaveTmpFile(filename string, data io.Reader) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAudioDuration returns the duration of an audio file in seconds.
|
// GetAudioDuration returns the duration of an audio file in seconds.
|
||||||
func GetAudioDuration(ctx context.Context, filename string) (float64, error) {
|
func GetAudioDuration(ctx context.Context, filename string, ext string) (float64, error) {
|
||||||
// ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {{input}}
|
// ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {{input}}
|
||||||
c := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
|
c := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
|
||||||
output, err := c.Output()
|
output, err := c.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.Wrap(err, "failed to get audio duration")
|
return 0, errors.Wrap(err, "failed to get audio duration")
|
||||||
}
|
}
|
||||||
|
durationStr := string(bytes.TrimSpace(output))
|
||||||
|
if durationStr == "N/A" {
|
||||||
|
// Create a temporary output file name
|
||||||
|
tmpFp, err := os.CreateTemp("", "audio-*"+ext)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "failed to create temporary file")
|
||||||
|
}
|
||||||
|
tmpName := tmpFp.Name()
|
||||||
|
// Close immediately so ffmpeg can open the file on Windows.
|
||||||
|
_ = tmpFp.Close()
|
||||||
|
defer os.Remove(tmpName)
|
||||||
|
|
||||||
return strconv.ParseFloat(string(bytes.TrimSpace(output)), 64)
|
// ffmpeg -y -i filename -vcodec copy -acodec copy <tmpName>
|
||||||
|
ffmpegCmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-i", filename, "-vcodec", "copy", "-acodec", "copy", tmpName)
|
||||||
|
if err := ffmpegCmd.Run(); err != nil {
|
||||||
|
return 0, errors.Wrap(err, "failed to run ffmpeg")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate the duration of the new file
|
||||||
|
c = exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", tmpName)
|
||||||
|
output, err := c.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "failed to get audio duration after ffmpeg")
|
||||||
|
}
|
||||||
|
durationStr = string(bytes.TrimSpace(output))
|
||||||
|
}
|
||||||
|
return strconv.ParseFloat(durationStr, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildURL concatenates base and endpoint, returns the complete url string
|
||||||
|
func BuildURL(base string, endpoint string) string {
|
||||||
|
u, err := url.Parse(base)
|
||||||
|
if err != nil {
|
||||||
|
return base + endpoint
|
||||||
|
}
|
||||||
|
end := endpoint
|
||||||
|
if end == "" {
|
||||||
|
end = "/"
|
||||||
|
}
|
||||||
|
ref, err := url.Parse(end)
|
||||||
|
if err != nil {
|
||||||
|
return base + endpoint
|
||||||
|
}
|
||||||
|
return u.ResolveReference(ref).String()
|
||||||
}
|
}
|
||||||
|
|||||||
26
constant/README.md
Normal file
26
constant/README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# constant 包 (`/constant`)
|
||||||
|
|
||||||
|
该目录仅用于放置全局可复用的**常量定义**,不包含任何业务逻辑或依赖关系。
|
||||||
|
|
||||||
|
## 当前文件
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|----------------------|---------------------------------------------------------------------|
|
||||||
|
| `azure.go` | 定义与 Azure 相关的全局常量,如 `AzureNoRemoveDotTime`(控制删除 `.` 的截止时间)。 |
|
||||||
|
| `cache_key.go` | 缓存键格式字符串及 Token 相关字段常量,统一缓存命名规则。 |
|
||||||
|
| `channel_setting.go` | Channel 级别的设置键,如 `proxy`、`force_format` 等。 |
|
||||||
|
| `context_key.go` | 定义 `ContextKey` 类型以及在整个项目中使用的上下文键常量(请求时间、Token/Channel/User 相关信息等)。 |
|
||||||
|
| `env.go` | 环境配置相关的全局变量,在启动阶段根据配置文件或环境变量注入。 |
|
||||||
|
| `finish_reason.go` | OpenAI/GPT 请求返回的 `finish_reason` 字符串常量集合。 |
|
||||||
|
| `midjourney.go` | Midjourney 相关错误码及动作(Action)常量与模型到动作的映射表。 |
|
||||||
|
| `setup.go` | 标识项目是否已完成初始化安装 (`Setup` 布尔值)。 |
|
||||||
|
| `task.go` | 各种任务(Task)平台、动作常量及模型与动作映射表,如 Suno、Midjourney 等。 |
|
||||||
|
| `user_setting.go` | 用户设置相关键常量以及通知类型(Email/Webhook)等。 |
|
||||||
|
|
||||||
|
## 使用约定
|
||||||
|
|
||||||
|
1. `constant` 包**只能被其他包引用**(import),**禁止在此包中引用项目内的其他自定义包**。如确有需要,仅允许引用 **Go 标准库**。
|
||||||
|
2. 不允许在此目录内编写任何与业务流程、数据库操作、第三方服务调用等相关的逻辑代码。
|
||||||
|
3. 新增类型时,请保持命名语义清晰,并在本 README 的 **当前文件** 表格中补充说明,确保团队成员能够快速了解其用途。
|
||||||
|
|
||||||
|
> ⚠️ 违反以上约定将导致包之间产生不必要的耦合,影响代码可维护性与可测试性。请在提交代码前自行检查。
|
||||||
34
constant/api_type.go
Normal file
34
constant/api_type.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
APITypeOpenAI = iota
|
||||||
|
APITypeAnthropic
|
||||||
|
APITypePaLM
|
||||||
|
APITypeBaidu
|
||||||
|
APITypeZhipu
|
||||||
|
APITypeAli
|
||||||
|
APITypeXunfei
|
||||||
|
APITypeAIProxyLibrary
|
||||||
|
APITypeTencent
|
||||||
|
APITypeGemini
|
||||||
|
APITypeZhipuV4
|
||||||
|
APITypeOllama
|
||||||
|
APITypePerplexity
|
||||||
|
APITypeAws
|
||||||
|
APITypeCohere
|
||||||
|
APITypeDify
|
||||||
|
APITypeJina
|
||||||
|
APITypeCloudflare
|
||||||
|
APITypeSiliconFlow
|
||||||
|
APITypeVertexAi
|
||||||
|
APITypeMistral
|
||||||
|
APITypeDeepSeek
|
||||||
|
APITypeMokaAI
|
||||||
|
APITypeVolcEngine
|
||||||
|
APITypeBaiduV2
|
||||||
|
APITypeOpenRouter
|
||||||
|
APITypeXinference
|
||||||
|
APITypeXai
|
||||||
|
APITypeCoze
|
||||||
|
APITypeDummy // this one is only for count, do not add any channel after this
|
||||||
|
)
|
||||||
5
constant/azure.go
Normal file
5
constant/azure.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
var AzureNoRemoveDotTime = time.Date(2025, time.May, 10, 0, 0, 0, 0, time.UTC).Unix()
|
||||||
@@ -1,14 +1,5 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
import "one-api/common"
|
|
||||||
|
|
||||||
var (
|
|
||||||
TokenCacheSeconds = common.SyncFrequency
|
|
||||||
UserId2GroupCacheSeconds = common.SyncFrequency
|
|
||||||
UserId2QuotaCacheSeconds = common.SyncFrequency
|
|
||||||
UserId2StatusCacheSeconds = common.SyncFrequency
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cache keys
|
// Cache keys
|
||||||
const (
|
const (
|
||||||
UserGroupKeyFmt = "user_group:%d"
|
UserGroupKeyFmt = "user_group:%d"
|
||||||
|
|||||||
109
constant/channel.go
Normal file
109
constant/channel.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChannelTypeUnknown = 0
|
||||||
|
ChannelTypeOpenAI = 1
|
||||||
|
ChannelTypeMidjourney = 2
|
||||||
|
ChannelTypeAzure = 3
|
||||||
|
ChannelTypeOllama = 4
|
||||||
|
ChannelTypeMidjourneyPlus = 5
|
||||||
|
ChannelTypeOpenAIMax = 6
|
||||||
|
ChannelTypeOhMyGPT = 7
|
||||||
|
ChannelTypeCustom = 8
|
||||||
|
ChannelTypeAILS = 9
|
||||||
|
ChannelTypeAIProxy = 10
|
||||||
|
ChannelTypePaLM = 11
|
||||||
|
ChannelTypeAPI2GPT = 12
|
||||||
|
ChannelTypeAIGC2D = 13
|
||||||
|
ChannelTypeAnthropic = 14
|
||||||
|
ChannelTypeBaidu = 15
|
||||||
|
ChannelTypeZhipu = 16
|
||||||
|
ChannelTypeAli = 17
|
||||||
|
ChannelTypeXunfei = 18
|
||||||
|
ChannelType360 = 19
|
||||||
|
ChannelTypeOpenRouter = 20
|
||||||
|
ChannelTypeAIProxyLibrary = 21
|
||||||
|
ChannelTypeFastGPT = 22
|
||||||
|
ChannelTypeTencent = 23
|
||||||
|
ChannelTypeGemini = 24
|
||||||
|
ChannelTypeMoonshot = 25
|
||||||
|
ChannelTypeZhipu_v4 = 26
|
||||||
|
ChannelTypePerplexity = 27
|
||||||
|
ChannelTypeLingYiWanWu = 31
|
||||||
|
ChannelTypeAws = 33
|
||||||
|
ChannelTypeCohere = 34
|
||||||
|
ChannelTypeMiniMax = 35
|
||||||
|
ChannelTypeSunoAPI = 36
|
||||||
|
ChannelTypeDify = 37
|
||||||
|
ChannelTypeJina = 38
|
||||||
|
ChannelCloudflare = 39
|
||||||
|
ChannelTypeSiliconFlow = 40
|
||||||
|
ChannelTypeVertexAi = 41
|
||||||
|
ChannelTypeMistral = 42
|
||||||
|
ChannelTypeDeepSeek = 43
|
||||||
|
ChannelTypeMokaAI = 44
|
||||||
|
ChannelTypeVolcEngine = 45
|
||||||
|
ChannelTypeBaiduV2 = 46
|
||||||
|
ChannelTypeXinference = 47
|
||||||
|
ChannelTypeXai = 48
|
||||||
|
ChannelTypeCoze = 49
|
||||||
|
ChannelTypeKling = 50
|
||||||
|
ChannelTypeJimeng = 51
|
||||||
|
ChannelTypeDummy // this one is only for count, do not add any channel after this
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
var ChannelBaseURLs = []string{
|
||||||
|
"", // 0
|
||||||
|
"https://api.openai.com", // 1
|
||||||
|
"https://oa.api2d.net", // 2
|
||||||
|
"", // 3
|
||||||
|
"http://localhost:11434", // 4
|
||||||
|
"https://api.openai-sb.com", // 5
|
||||||
|
"https://api.openaimax.com", // 6
|
||||||
|
"https://api.ohmygpt.com", // 7
|
||||||
|
"", // 8
|
||||||
|
"https://api.caipacity.com", // 9
|
||||||
|
"https://api.aiproxy.io", // 10
|
||||||
|
"", // 11
|
||||||
|
"https://api.api2gpt.com", // 12
|
||||||
|
"https://api.aigc2d.com", // 13
|
||||||
|
"https://api.anthropic.com", // 14
|
||||||
|
"https://aip.baidubce.com", // 15
|
||||||
|
"https://open.bigmodel.cn", // 16
|
||||||
|
"https://dashscope.aliyuncs.com", // 17
|
||||||
|
"", // 18
|
||||||
|
"https://api.360.cn", // 19
|
||||||
|
"https://openrouter.ai/api", // 20
|
||||||
|
"https://api.aiproxy.io", // 21
|
||||||
|
"https://fastgpt.run/api/openapi", // 22
|
||||||
|
"https://hunyuan.tencentcloudapi.com", //23
|
||||||
|
"https://generativelanguage.googleapis.com", //24
|
||||||
|
"https://api.moonshot.cn", //25
|
||||||
|
"https://open.bigmodel.cn", //26
|
||||||
|
"https://api.perplexity.ai", //27
|
||||||
|
"", //28
|
||||||
|
"", //29
|
||||||
|
"", //30
|
||||||
|
"https://api.lingyiwanwu.com", //31
|
||||||
|
"", //32
|
||||||
|
"", //33
|
||||||
|
"https://api.cohere.ai", //34
|
||||||
|
"https://api.minimax.chat", //35
|
||||||
|
"", //36
|
||||||
|
"https://api.dify.ai", //37
|
||||||
|
"https://api.jina.ai", //38
|
||||||
|
"https://api.cloudflare.com", //39
|
||||||
|
"https://api.siliconflow.cn", //40
|
||||||
|
"", //41
|
||||||
|
"https://api.mistral.ai", //42
|
||||||
|
"https://api.deepseek.com", //43
|
||||||
|
"https://api.moka.ai", //44
|
||||||
|
"https://ark.cn-beijing.volces.com", //45
|
||||||
|
"https://qianfan.baidubce.com", //46
|
||||||
|
"", //47
|
||||||
|
"https://api.x.ai", //48
|
||||||
|
"https://api.coze.cn", //49
|
||||||
|
"https://api.klingai.com", //50
|
||||||
|
"https://visual.volcengineapi.com", //51
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
var (
|
|
||||||
ForceFormat = "force_format" // ForceFormat 强制格式化为OpenAI格式
|
|
||||||
ChanelSettingProxy = "proxy" // Proxy 代理
|
|
||||||
ChannelSettingThinkingToContent = "thinking_to_content" // ThinkingToContent
|
|
||||||
)
|
|
||||||
@@ -1,5 +1,35 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ContextKeyRequestStartTime = "request_start_time"
|
ContextKeyOriginalModel ContextKey = "original_model"
|
||||||
|
ContextKeyRequestStartTime ContextKey = "request_start_time"
|
||||||
|
|
||||||
|
/* token related keys */
|
||||||
|
ContextKeyTokenUnlimited ContextKey = "token_unlimited_quota"
|
||||||
|
ContextKeyTokenKey ContextKey = "token_key"
|
||||||
|
ContextKeyTokenId ContextKey = "token_id"
|
||||||
|
ContextKeyTokenGroup ContextKey = "token_group"
|
||||||
|
ContextKeyTokenAllowIps ContextKey = "allow_ips"
|
||||||
|
ContextKeyTokenSpecificChannelId ContextKey = "specific_channel_id"
|
||||||
|
ContextKeyTokenModelLimitEnabled ContextKey = "token_model_limit_enabled"
|
||||||
|
ContextKeyTokenModelLimit ContextKey = "token_model_limit"
|
||||||
|
|
||||||
|
/* channel related keys */
|
||||||
|
ContextKeyBaseUrl ContextKey = "base_url"
|
||||||
|
ContextKeyChannelType ContextKey = "channel_type"
|
||||||
|
ContextKeyChannelId ContextKey = "channel_id"
|
||||||
|
ContextKeyChannelSetting ContextKey = "channel_setting"
|
||||||
|
ContextKeyParamOverride ContextKey = "param_override"
|
||||||
|
|
||||||
|
/* user related keys */
|
||||||
|
ContextKeyUserId ContextKey = "id"
|
||||||
|
ContextKeyUserSetting ContextKey = "user_setting"
|
||||||
|
ContextKeyUserQuota ContextKey = "user_quota"
|
||||||
|
ContextKeyUserStatus ContextKey = "user_status"
|
||||||
|
ContextKeyUserEmail ContextKey = "user_email"
|
||||||
|
ContextKeyUserGroup ContextKey = "user_group"
|
||||||
|
ContextKeyUsingGroup ContextKey = "group"
|
||||||
|
ContextKeyUserName ContextKey = "username"
|
||||||
)
|
)
|
||||||
|
|||||||
16
constant/endpoint_type.go
Normal file
16
constant/endpoint_type.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
type EndpointType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EndpointTypeOpenAI EndpointType = "openai"
|
||||||
|
EndpointTypeOpenAIResponse EndpointType = "openai-response"
|
||||||
|
EndpointTypeAnthropic EndpointType = "anthropic"
|
||||||
|
EndpointTypeGemini EndpointType = "gemini"
|
||||||
|
EndpointTypeJinaRerank EndpointType = "jina-rerank"
|
||||||
|
EndpointTypeImageGeneration EndpointType = "image-generation"
|
||||||
|
//EndpointTypeMidjourney EndpointType = "midjourney-proxy"
|
||||||
|
//EndpointTypeSuno EndpointType = "suno-proxy"
|
||||||
|
//EndpointTypeKling EndpointType = "kling"
|
||||||
|
//EndpointTypeJimeng EndpointType = "jimeng"
|
||||||
|
)
|
||||||
@@ -1,51 +1,15 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
import (
|
var StreamingTimeout int
|
||||||
"fmt"
|
var DifyDebug bool
|
||||||
"one-api/common"
|
var MaxFileDownloadMB int
|
||||||
"os"
|
var ForceStreamOption bool
|
||||||
"strings"
|
var GetMediaToken bool
|
||||||
)
|
var GetMediaTokenNotStream bool
|
||||||
|
var UpdateTask bool
|
||||||
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 60)
|
var AzureDefaultAPIVersion string
|
||||||
var DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true)
|
var GeminiVisionMaxImageNum int
|
||||||
|
var NotifyLimitCount int
|
||||||
var MaxFileDownloadMB = common.GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20)
|
var NotificationLimitDurationMinute int
|
||||||
|
var GenerateDefaultToken bool
|
||||||
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
var ErrorLogEnabled bool
|
||||||
var ForceStreamOption = common.GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true)
|
|
||||||
|
|
||||||
var GetMediaToken = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true)
|
|
||||||
|
|
||||||
var GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true)
|
|
||||||
|
|
||||||
var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
|
|
||||||
|
|
||||||
var AzureDefaultAPIVersion = common.GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2024-12-01-preview")
|
|
||||||
|
|
||||||
var GeminiModelMap = map[string]string{
|
|
||||||
"gemini-1.0-pro": "v1",
|
|
||||||
}
|
|
||||||
|
|
||||||
var GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
|
|
||||||
|
|
||||||
var NotifyLimitCount = common.GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2)
|
|
||||||
var NotificationLimitDurationMinute = common.GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
|
|
||||||
|
|
||||||
func InitEnv() {
|
|
||||||
modelVersionMapStr := strings.TrimSpace(os.Getenv("GEMINI_MODEL_MAP"))
|
|
||||||
if modelVersionMapStr == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, pair := range strings.Split(modelVersionMapStr, ",") {
|
|
||||||
parts := strings.Split(pair, ":")
|
|
||||||
if len(parts) == 2 {
|
|
||||||
GeminiModelMap[parts[0]] = parts[1]
|
|
||||||
} else {
|
|
||||||
common.SysError(fmt.Sprintf("invalid model version map: %s", pair))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateDefaultToken 是否生成初始令牌,默认关闭。
|
|
||||||
var GenerateDefaultToken = common.GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const (
|
|||||||
MjActionPan = "PAN"
|
MjActionPan = "PAN"
|
||||||
MjActionSwapFace = "SWAP_FACE"
|
MjActionSwapFace = "SWAP_FACE"
|
||||||
MjActionUpload = "UPLOAD"
|
MjActionUpload = "UPLOAD"
|
||||||
|
MjActionVideo = "VIDEO"
|
||||||
|
MjActionEdits = "EDITS"
|
||||||
)
|
)
|
||||||
|
|
||||||
var MidjourneyModel2Action = map[string]string{
|
var MidjourneyModel2Action = map[string]string{
|
||||||
@@ -41,4 +43,6 @@ var MidjourneyModel2Action = map[string]string{
|
|||||||
"mj_pan": MjActionPan,
|
"mj_pan": MjActionPan,
|
||||||
"swap_face": MjActionSwapFace,
|
"swap_face": MjActionSwapFace,
|
||||||
"mj_upload": MjActionUpload,
|
"mj_upload": MjActionUpload,
|
||||||
|
"mj_video": MjActionVideo,
|
||||||
|
"mj_edits": MjActionEdits,
|
||||||
}
|
}
|
||||||
|
|||||||
3
constant/setup.go
Normal file
3
constant/setup.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
var Setup = false
|
||||||
@@ -5,11 +5,16 @@ type TaskPlatform string
|
|||||||
const (
|
const (
|
||||||
TaskPlatformSuno TaskPlatform = "suno"
|
TaskPlatformSuno TaskPlatform = "suno"
|
||||||
TaskPlatformMidjourney = "mj"
|
TaskPlatformMidjourney = "mj"
|
||||||
|
TaskPlatformKling TaskPlatform = "kling"
|
||||||
|
TaskPlatformJimeng TaskPlatform = "jimeng"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SunoActionMusic = "MUSIC"
|
SunoActionMusic = "MUSIC"
|
||||||
SunoActionLyrics = "LYRICS"
|
SunoActionLyrics = "LYRICS"
|
||||||
|
|
||||||
|
TaskActionGenerate = "generate"
|
||||||
|
TaskActionTextGenerate = "textGenerate"
|
||||||
)
|
)
|
||||||
|
|
||||||
var SunoModel2Action = map[string]string{
|
var SunoModel2Action = map[string]string{
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
var (
|
|
||||||
UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型
|
|
||||||
UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值
|
|
||||||
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
|
|
||||||
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
|
|
||||||
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
NotifyTypeEmail = "email" // Email 邮件
|
|
||||||
NotifyTypeWebhook = "webhook" // Webhook
|
|
||||||
)
|
|
||||||
@@ -4,11 +4,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
|
"one-api/setting"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -108,6 +111,13 @@ type DeepSeekUsageResponse struct {
|
|||||||
} `json:"balance_infos"`
|
} `json:"balance_infos"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenRouterCreditResponse struct {
|
||||||
|
Data struct {
|
||||||
|
TotalCredits float64 `json:"total_credits"`
|
||||||
|
TotalUsage float64 `json:"total_usage"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetAuthHeader get auth header
|
// GetAuthHeader get auth header
|
||||||
func GetAuthHeader(token string) http.Header {
|
func GetAuthHeader(token string) http.Header {
|
||||||
h := http.Header{}
|
h := http.Header{}
|
||||||
@@ -281,32 +291,86 @@ func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) {
|
|||||||
return response.TotalAvailable, nil
|
return response.TotalAvailable, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) {
|
||||||
|
url := "https://openrouter.ai/api/v1/credits"
|
||||||
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
response := OpenRouterCreditResponse{}
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
balance := response.Data.TotalCredits - response.Data.TotalUsage
|
||||||
|
channel.UpdateBalance(balance)
|
||||||
|
return balance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) {
|
||||||
|
url := "https://api.moonshot.cn/v1/users/me/balance"
|
||||||
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoonshotBalanceData struct {
|
||||||
|
AvailableBalance float64 `json:"available_balance"`
|
||||||
|
VoucherBalance float64 `json:"voucher_balance"`
|
||||||
|
CashBalance float64 `json:"cash_balance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoonshotBalanceResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data MoonshotBalanceData `json:"data"`
|
||||||
|
Scode string `json:"scode"`
|
||||||
|
Status bool `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
response := MoonshotBalanceResponse{}
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if !response.Status || response.Code != 0 {
|
||||||
|
return 0, fmt.Errorf("failed to update moonshot balance, status: %v, code: %d, scode: %s", response.Status, response.Code, response.Scode)
|
||||||
|
}
|
||||||
|
availableBalanceCny := response.Data.AvailableBalance
|
||||||
|
availableBalanceUsd := decimal.NewFromFloat(availableBalanceCny).Div(decimal.NewFromFloat(setting.Price)).InexactFloat64()
|
||||||
|
channel.UpdateBalance(availableBalanceUsd)
|
||||||
|
return availableBalanceUsd, nil
|
||||||
|
}
|
||||||
|
|
||||||
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||||
baseURL := common.ChannelBaseURLs[channel.Type]
|
baseURL := constant.ChannelBaseURLs[channel.Type]
|
||||||
if channel.GetBaseURL() == "" {
|
if channel.GetBaseURL() == "" {
|
||||||
channel.BaseURL = &baseURL
|
channel.BaseURL = &baseURL
|
||||||
}
|
}
|
||||||
switch channel.Type {
|
switch channel.Type {
|
||||||
case common.ChannelTypeOpenAI:
|
case constant.ChannelTypeOpenAI:
|
||||||
if channel.GetBaseURL() != "" {
|
if channel.GetBaseURL() != "" {
|
||||||
baseURL = channel.GetBaseURL()
|
baseURL = channel.GetBaseURL()
|
||||||
}
|
}
|
||||||
case common.ChannelTypeAzure:
|
case constant.ChannelTypeAzure:
|
||||||
return 0, errors.New("尚未实现")
|
return 0, errors.New("尚未实现")
|
||||||
case common.ChannelTypeCustom:
|
case constant.ChannelTypeCustom:
|
||||||
baseURL = channel.GetBaseURL()
|
baseURL = channel.GetBaseURL()
|
||||||
//case common.ChannelTypeOpenAISB:
|
//case common.ChannelTypeOpenAISB:
|
||||||
// return updateChannelOpenAISBBalance(channel)
|
// return updateChannelOpenAISBBalance(channel)
|
||||||
case common.ChannelTypeAIProxy:
|
case constant.ChannelTypeAIProxy:
|
||||||
return updateChannelAIProxyBalance(channel)
|
return updateChannelAIProxyBalance(channel)
|
||||||
case common.ChannelTypeAPI2GPT:
|
case constant.ChannelTypeAPI2GPT:
|
||||||
return updateChannelAPI2GPTBalance(channel)
|
return updateChannelAPI2GPTBalance(channel)
|
||||||
case common.ChannelTypeAIGC2D:
|
case constant.ChannelTypeAIGC2D:
|
||||||
return updateChannelAIGC2DBalance(channel)
|
return updateChannelAIGC2DBalance(channel)
|
||||||
case common.ChannelTypeSiliconFlow:
|
case constant.ChannelTypeSiliconFlow:
|
||||||
return updateChannelSiliconFlowBalance(channel)
|
return updateChannelSiliconFlowBalance(channel)
|
||||||
case common.ChannelTypeDeepSeek:
|
case constant.ChannelTypeDeepSeek:
|
||||||
return updateChannelDeepSeekBalance(channel)
|
return updateChannelDeepSeekBalance(channel)
|
||||||
|
case constant.ChannelTypeOpenRouter:
|
||||||
|
return updateChannelOpenRouterBalance(channel)
|
||||||
|
case constant.ChannelTypeMoonshot:
|
||||||
|
return updateChannelMoonshotBalance(channel)
|
||||||
default:
|
default:
|
||||||
return 0, errors.New("尚未实现")
|
return 0, errors.New("尚未实现")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/middleware"
|
"one-api/middleware"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/relay"
|
"one-api/relay"
|
||||||
relaycommon "one-api/relay/common"
|
relaycommon "one-api/relay/common"
|
||||||
"one-api/relay/constant"
|
"one-api/relay/helper"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -30,15 +31,21 @@ import (
|
|||||||
|
|
||||||
func testChannel(channel *model.Channel, testModel string) (err error, openAIErrorWithStatusCode *dto.OpenAIErrorWithStatusCode) {
|
func testChannel(channel *model.Channel, testModel string) (err error, openAIErrorWithStatusCode *dto.OpenAIErrorWithStatusCode) {
|
||||||
tik := time.Now()
|
tik := time.Now()
|
||||||
if channel.Type == common.ChannelTypeMidjourney {
|
if channel.Type == constant.ChannelTypeMidjourney {
|
||||||
return errors.New("midjourney channel test is not supported"), nil
|
return errors.New("midjourney channel test is not supported"), nil
|
||||||
}
|
}
|
||||||
if channel.Type == common.ChannelTypeMidjourneyPlus {
|
if channel.Type == constant.ChannelTypeMidjourneyPlus {
|
||||||
return errors.New("midjourney plus channel test is not supported!!!"), nil
|
return errors.New("midjourney plus channel test is not supported"), nil
|
||||||
}
|
}
|
||||||
if channel.Type == common.ChannelTypeSunoAPI {
|
if channel.Type == constant.ChannelTypeSunoAPI {
|
||||||
return errors.New("suno channel test is not supported"), nil
|
return errors.New("suno channel test is not supported"), nil
|
||||||
}
|
}
|
||||||
|
if channel.Type == constant.ChannelTypeKling {
|
||||||
|
return errors.New("kling channel test is not supported"), nil
|
||||||
|
}
|
||||||
|
if channel.Type == constant.ChannelTypeJimeng {
|
||||||
|
return errors.New("jimeng channel test is not supported"), nil
|
||||||
|
}
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := gin.CreateTestContext(w)
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
|
||||||
@@ -48,8 +55,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|||||||
if strings.Contains(strings.ToLower(testModel), "embedding") ||
|
if strings.Contains(strings.ToLower(testModel), "embedding") ||
|
||||||
strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
|
strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
|
||||||
strings.Contains(testModel, "bge-") || // bge 系列模型
|
strings.Contains(testModel, "bge-") || // bge 系列模型
|
||||||
testModel == "text-embedding-v1" ||
|
strings.Contains(testModel, "embed") ||
|
||||||
channel.Type == common.ChannelTypeMokaAI { // 其他 embedding 模型
|
channel.Type == constant.ChannelTypeMokaAI { // 其他 embedding 模型
|
||||||
requestPath = "/v1/embeddings" // 修改请求路径
|
requestPath = "/v1/embeddings" // 修改请求路径
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,39 +79,49 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modelMapping := *channel.ModelMapping
|
cache, err := model.GetUserCache(1)
|
||||||
if modelMapping != "" && modelMapping != "{}" {
|
if err != nil {
|
||||||
modelMap := make(map[string]string)
|
return err, nil
|
||||||
err := json.Unmarshal([]byte(modelMapping), &modelMap)
|
|
||||||
if err != nil {
|
|
||||||
return err, service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
if modelMap[testModel] != "" {
|
|
||||||
testModel = modelMap[testModel]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
cache.WriteContext(c)
|
||||||
|
|
||||||
c.Request.Header.Set("Authorization", "Bearer "+channel.Key)
|
c.Request.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||||
c.Request.Header.Set("Content-Type", "application/json")
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
c.Set("channel", channel.Type)
|
c.Set("channel", channel.Type)
|
||||||
c.Set("base_url", channel.GetBaseURL())
|
c.Set("base_url", channel.GetBaseURL())
|
||||||
|
group, _ := model.GetUserGroup(1, false)
|
||||||
|
c.Set("group", group)
|
||||||
|
|
||||||
middleware.SetupContextForSelectedChannel(c, channel, testModel)
|
middleware.SetupContextForSelectedChannel(c, channel, testModel)
|
||||||
|
|
||||||
meta := relaycommon.GenRelayInfo(c)
|
info := relaycommon.GenRelayInfo(c)
|
||||||
apiType, _ := constant.ChannelType2APIType(channel.Type)
|
|
||||||
|
err = helper.ModelMappedHelper(c, info, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err, nil
|
||||||
|
}
|
||||||
|
testModel = info.UpstreamModelName
|
||||||
|
|
||||||
|
apiType, _ := common.ChannelType2APIType(channel.Type)
|
||||||
adaptor := relay.GetAdaptor(apiType)
|
adaptor := relay.GetAdaptor(apiType)
|
||||||
if adaptor == nil {
|
if adaptor == nil {
|
||||||
return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
|
return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
request := buildTestRequest(testModel)
|
request := buildTestRequest(testModel)
|
||||||
meta.UpstreamModelName = testModel
|
// 创建一个用于日志的 info 副本,移除 ApiKey
|
||||||
common.SysLog(fmt.Sprintf("testing channel %d with model %s , meta %v ", channel.Id, testModel, meta))
|
logInfo := *info
|
||||||
|
logInfo.ApiKey = ""
|
||||||
|
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %+v ", channel.Id, testModel, logInfo))
|
||||||
|
|
||||||
adaptor.Init(meta)
|
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens))
|
||||||
|
if err != nil {
|
||||||
|
return err, nil
|
||||||
|
}
|
||||||
|
|
||||||
convertedRequest, err := adaptor.ConvertRequest(c, meta, request)
|
adaptor.Init(info)
|
||||||
|
|
||||||
|
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, nil
|
return err, nil
|
||||||
}
|
}
|
||||||
@@ -114,7 +131,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|||||||
}
|
}
|
||||||
requestBody := bytes.NewBuffer(jsonData)
|
requestBody := bytes.NewBuffer(jsonData)
|
||||||
c.Request.Body = io.NopCloser(requestBody)
|
c.Request.Body = io.NopCloser(requestBody)
|
||||||
resp, err := adaptor.DoRequest(c, meta, requestBody)
|
resp, err := adaptor.DoRequest(c, info, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, nil
|
return err, nil
|
||||||
}
|
}
|
||||||
@@ -122,11 +139,11 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|||||||
if resp != nil {
|
if resp != nil {
|
||||||
httpResp = resp.(*http.Response)
|
httpResp = resp.(*http.Response)
|
||||||
if httpResp.StatusCode != http.StatusOK {
|
if httpResp.StatusCode != http.StatusOK {
|
||||||
err := service.RelayErrorHandler(httpResp)
|
err := service.RelayErrorHandler(httpResp, true)
|
||||||
return fmt.Errorf("status code %d: %s", httpResp.StatusCode, err.Error.Message), err
|
return fmt.Errorf("status code %d: %s", httpResp.StatusCode, err.Error.Message), err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
usageA, respErr := adaptor.DoResponse(c, httpResp, meta)
|
usageA, respErr := adaptor.DoResponse(c, httpResp, info)
|
||||||
if respErr != nil {
|
if respErr != nil {
|
||||||
return fmt.Errorf("%s", respErr.Error.Message), respErr
|
return fmt.Errorf("%s", respErr.Error.Message), respErr
|
||||||
}
|
}
|
||||||
@@ -139,26 +156,36 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err, nil
|
return err, nil
|
||||||
}
|
}
|
||||||
modelPrice, usePrice := common.GetModelPrice(testModel, false)
|
info.PromptTokens = usage.PromptTokens
|
||||||
modelRatio := common.GetModelRatio(testModel)
|
|
||||||
completionRatio := common.GetCompletionRatio(testModel)
|
|
||||||
ratio := modelRatio
|
|
||||||
quota := 0
|
quota := 0
|
||||||
if !usePrice {
|
if !priceData.UsePrice {
|
||||||
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*completionRatio))
|
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio))
|
||||||
quota = int(math.Round(float64(quota) * ratio))
|
quota = int(math.Round(float64(quota) * priceData.ModelRatio))
|
||||||
if ratio != 0 && quota <= 0 {
|
if priceData.ModelRatio != 0 && quota <= 0 {
|
||||||
quota = 1
|
quota = 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
quota = int(modelPrice * common.QuotaPerUnit)
|
quota = int(priceData.ModelPrice * common.QuotaPerUnit)
|
||||||
}
|
}
|
||||||
tok := time.Now()
|
tok := time.Now()
|
||||||
milliseconds := tok.Sub(tik).Milliseconds()
|
milliseconds := tok.Sub(tik).Milliseconds()
|
||||||
consumedTime := float64(milliseconds) / 1000.0
|
consumedTime := float64(milliseconds) / 1000.0
|
||||||
other := service.GenerateTextOtherInfo(c, meta, modelRatio, 1, completionRatio, modelPrice)
|
other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatioInfo.GroupRatio, priceData.CompletionRatio,
|
||||||
model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, testModel, "模型测试",
|
usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice, priceData.GroupRatioInfo.GroupSpecialRatio)
|
||||||
quota, "模型测试", 0, quota, int(consumedTime), false, "default", other)
|
model.RecordConsumeLog(c, 1, model.RecordConsumeLogParams{
|
||||||
|
ChannelId: channel.Id,
|
||||||
|
PromptTokens: usage.PromptTokens,
|
||||||
|
CompletionTokens: usage.CompletionTokens,
|
||||||
|
ModelName: info.OriginModelName,
|
||||||
|
TokenName: "模型测试",
|
||||||
|
Quota: quota,
|
||||||
|
Content: "模型测试",
|
||||||
|
UseTimeSeconds: int(consumedTime),
|
||||||
|
IsStream: false,
|
||||||
|
Group: info.UsingGroup,
|
||||||
|
Other: other,
|
||||||
|
})
|
||||||
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
|
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -170,24 +197,30 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 先判断是否为 Embedding 模型
|
// 先判断是否为 Embedding 模型
|
||||||
if strings.Contains(strings.ToLower(model), "embedding") ||
|
if strings.Contains(strings.ToLower(model), "embedding") || // 其他 embedding 模型
|
||||||
strings.HasPrefix(model, "m3e") || // m3e 系列模型
|
strings.HasPrefix(model, "m3e") || // m3e 系列模型
|
||||||
strings.Contains(model, "bge-") || // bge 系列模型
|
strings.Contains(model, "bge-") {
|
||||||
model == "text-embedding-v1" { // 其他 embedding 模型
|
testRequest.Model = model
|
||||||
// Embedding 请求
|
// Embedding 请求
|
||||||
testRequest.Input = []string{"hello world"}
|
testRequest.Input = []string{"hello world"}
|
||||||
return testRequest
|
return testRequest
|
||||||
}
|
}
|
||||||
// 并非Embedding 模型
|
// 并非Embedding 模型
|
||||||
if strings.HasPrefix(model, "o1") || strings.HasPrefix(model, "o3") {
|
if strings.HasPrefix(model, "o") {
|
||||||
testRequest.MaxCompletionTokens = 10
|
testRequest.MaxCompletionTokens = 10
|
||||||
|
} else if strings.Contains(model, "thinking") {
|
||||||
|
if !strings.Contains(model, "claude") {
|
||||||
|
testRequest.MaxTokens = 50
|
||||||
|
}
|
||||||
|
} else if strings.Contains(model, "gemini") {
|
||||||
|
testRequest.MaxTokens = 3000
|
||||||
} else {
|
} else {
|
||||||
testRequest.MaxTokens = 10
|
testRequest.MaxTokens = 10
|
||||||
}
|
}
|
||||||
content, _ := json.Marshal("hi")
|
|
||||||
testMessage := dto.Message{
|
testMessage := dto.Message{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: content,
|
Content: "hi",
|
||||||
}
|
}
|
||||||
testRequest.Model = model
|
testRequest.Model = model
|
||||||
testRequest.Messages = append(testRequest.Messages, testMessage)
|
testRequest.Messages = append(testRequest.Messages, testMessage)
|
||||||
@@ -255,6 +288,13 @@ func testAllChannels(notify bool) error {
|
|||||||
disableThreshold = 10000000 // a impossible value
|
disableThreshold = 10000000 // a impossible value
|
||||||
}
|
}
|
||||||
gopool.Go(func() {
|
gopool.Go(func() {
|
||||||
|
// 使用 defer 确保无论如何都会重置运行状态,防止死锁
|
||||||
|
defer func() {
|
||||||
|
testAllChannelsLock.Lock()
|
||||||
|
testAllChannelsRunning = false
|
||||||
|
testAllChannelsLock.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||||
tik := time.Now()
|
tik := time.Now()
|
||||||
@@ -289,9 +329,7 @@ func testAllChannels(notify bool) error {
|
|||||||
channel.UpdateResponseTime(milliseconds)
|
channel.UpdateResponseTime(milliseconds)
|
||||||
time.Sleep(common.RequestInterval)
|
time.Sleep(common.RequestInterval)
|
||||||
}
|
}
|
||||||
testAllChannelsLock.Lock()
|
|
||||||
testAllChannelsRunning = false
|
|
||||||
testAllChannelsLock.Unlock()
|
|
||||||
if notify {
|
if notify {
|
||||||
service.NotifyRootUser(dto.NotifyTypeChannelTest, "通道测试完成", "所有通道测试已完成")
|
service.NotifyRootUser(dto.NotifyTypeChannelTest, "通道测试完成", "所有通道测试已完成")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -40,50 +41,124 @@ type OpenAIModelsResponse struct {
|
|||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseStatusFilter(statusParam string) int {
|
||||||
|
switch strings.ToLower(statusParam) {
|
||||||
|
case "enabled", "1":
|
||||||
|
return common.ChannelStatusEnabled
|
||||||
|
case "disabled", "0":
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetAllChannels(c *gin.Context) {
|
func GetAllChannels(c *gin.Context) {
|
||||||
p, _ := strconv.Atoi(c.Query("p"))
|
p, _ := strconv.Atoi(c.Query("p"))
|
||||||
pageSize, _ := strconv.Atoi(c.Query("page_size"))
|
pageSize, _ := strconv.Atoi(c.Query("page_size"))
|
||||||
if p < 0 {
|
if p < 1 {
|
||||||
p = 0
|
p = 1
|
||||||
}
|
}
|
||||||
if pageSize < 0 {
|
if pageSize < 1 {
|
||||||
pageSize = common.ItemsPerPage
|
pageSize = common.ItemsPerPage
|
||||||
}
|
}
|
||||||
channelData := make([]*model.Channel, 0)
|
channelData := make([]*model.Channel, 0)
|
||||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||||
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
||||||
|
statusParam := c.Query("status")
|
||||||
|
// statusFilter: -1 all, 1 enabled, 0 disabled (include auto & manual)
|
||||||
|
statusFilter := parseStatusFilter(statusParam)
|
||||||
|
// type filter
|
||||||
|
typeStr := c.Query("type")
|
||||||
|
typeFilter := -1
|
||||||
|
if typeStr != "" {
|
||||||
|
if t, err := strconv.Atoi(typeStr); err == nil {
|
||||||
|
typeFilter = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
|
||||||
if enableTagMode {
|
if enableTagMode {
|
||||||
tags, err := model.GetPaginatedTags(p*pageSize, pageSize)
|
tags, err := model.GetPaginatedTags((p-1)*pageSize, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
if tag != nil && *tag != "" {
|
if tag == nil || *tag == "" {
|
||||||
tagChannel, err := model.GetChannelsByTag(*tag, idSort)
|
continue
|
||||||
if err == nil {
|
|
||||||
channelData = append(channelData, tagChannel...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
tagChannels, err := model.GetChannelsByTag(*tag, idSort)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered := make([]*model.Channel, 0)
|
||||||
|
for _, ch := range tagChannels {
|
||||||
|
if statusFilter == common.ChannelStatusEnabled && ch.Status != common.ChannelStatusEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if statusFilter == 0 && ch.Status == common.ChannelStatusEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if typeFilter >= 0 && ch.Type != typeFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, ch)
|
||||||
|
}
|
||||||
|
channelData = append(channelData, filtered...)
|
||||||
}
|
}
|
||||||
|
total, _ = model.CountAllTags()
|
||||||
} else {
|
} else {
|
||||||
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
|
baseQuery := model.DB.Model(&model.Channel{})
|
||||||
|
if typeFilter >= 0 {
|
||||||
|
baseQuery = baseQuery.Where("type = ?", typeFilter)
|
||||||
|
}
|
||||||
|
if statusFilter == common.ChannelStatusEnabled {
|
||||||
|
baseQuery = baseQuery.Where("status = ?", common.ChannelStatusEnabled)
|
||||||
|
} else if statusFilter == 0 {
|
||||||
|
baseQuery = baseQuery.Where("status != ?", common.ChannelStatusEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseQuery.Count(&total)
|
||||||
|
|
||||||
|
order := "priority desc"
|
||||||
|
if idSort {
|
||||||
|
order = "id desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := baseQuery.Order(order).Limit(pageSize).Offset((p - 1) * pageSize).Omit("key").Find(&channelData).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
channelData = channels
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countQuery := model.DB.Model(&model.Channel{})
|
||||||
|
if statusFilter == common.ChannelStatusEnabled {
|
||||||
|
countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled)
|
||||||
|
} else if statusFilter == 0 {
|
||||||
|
countQuery = countQuery.Where("status != ?", common.ChannelStatusEnabled)
|
||||||
|
}
|
||||||
|
var results []struct {
|
||||||
|
Type int64
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
_ = countQuery.Select("type, count(*) as count").Group("type").Find(&results).Error
|
||||||
|
typeCounts := make(map[int64]int64)
|
||||||
|
for _, r := range results {
|
||||||
|
typeCounts[r.Type] = r.Count
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": channelData,
|
"data": gin.H{
|
||||||
|
"items": channelData,
|
||||||
|
"total": total,
|
||||||
|
"page": p,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"type_counts": typeCounts,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -107,18 +182,17 @@ func FetchUpstreamModels(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//if channel.Type != common.ChannelTypeOpenAI {
|
baseURL := constant.ChannelBaseURLs[channel.Type]
|
||||||
// c.JSON(http.StatusOK, gin.H{
|
|
||||||
// "success": false,
|
|
||||||
// "message": "仅支持 OpenAI 类型渠道",
|
|
||||||
// })
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
baseURL := common.ChannelBaseURLs[channel.Type]
|
|
||||||
if channel.GetBaseURL() != "" {
|
if channel.GetBaseURL() != "" {
|
||||||
baseURL = channel.GetBaseURL()
|
baseURL = channel.GetBaseURL()
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("%s/v1/models", baseURL)
|
url := fmt.Sprintf("%s/v1/models", baseURL)
|
||||||
|
switch channel.Type {
|
||||||
|
case constant.ChannelTypeGemini:
|
||||||
|
url = fmt.Sprintf("%s/v1beta/openai/models", baseURL)
|
||||||
|
case constant.ChannelTypeAli:
|
||||||
|
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
|
||||||
|
}
|
||||||
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -139,7 +213,11 @@ func FetchUpstreamModels(c *gin.Context) {
|
|||||||
|
|
||||||
var ids []string
|
var ids []string
|
||||||
for _, model := range result.Data {
|
for _, model := range result.Data {
|
||||||
ids = append(ids, model.ID)
|
id := model.ID
|
||||||
|
if channel.Type == constant.ChannelTypeGemini {
|
||||||
|
id = strings.TrimPrefix(id, "models/")
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -150,7 +228,7 @@ func FetchUpstreamModels(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FixChannelsAbilities(c *gin.Context) {
|
func FixChannelsAbilities(c *gin.Context) {
|
||||||
count, err := model.FixAbility()
|
success, fails, err := model.FixAbility()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -161,7 +239,10 @@ func FixChannelsAbilities(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": count,
|
"data": gin.H{
|
||||||
|
"success": success,
|
||||||
|
"fails": fails,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +250,8 @@ func SearchChannels(c *gin.Context) {
|
|||||||
keyword := c.Query("keyword")
|
keyword := c.Query("keyword")
|
||||||
group := c.Query("group")
|
group := c.Query("group")
|
||||||
modelKeyword := c.Query("model")
|
modelKeyword := c.Query("model")
|
||||||
|
statusParam := c.Query("status")
|
||||||
|
statusFilter := parseStatusFilter(statusParam)
|
||||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||||
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
||||||
channelData := make([]*model.Channel, 0)
|
channelData := make([]*model.Channel, 0)
|
||||||
@@ -200,10 +283,74 @@ func SearchChannels(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
channelData = channels
|
channelData = channels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if statusFilter == common.ChannelStatusEnabled || statusFilter == 0 {
|
||||||
|
filtered := make([]*model.Channel, 0, len(channelData))
|
||||||
|
for _, ch := range channelData {
|
||||||
|
if statusFilter == common.ChannelStatusEnabled && ch.Status != common.ChannelStatusEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if statusFilter == 0 && ch.Status == common.ChannelStatusEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, ch)
|
||||||
|
}
|
||||||
|
channelData = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate type counts for search results
|
||||||
|
typeCounts := make(map[int64]int64)
|
||||||
|
for _, channel := range channelData {
|
||||||
|
typeCounts[int64(channel.Type)]++
|
||||||
|
}
|
||||||
|
|
||||||
|
typeParam := c.Query("type")
|
||||||
|
typeFilter := -1
|
||||||
|
if typeParam != "" {
|
||||||
|
if tp, err := strconv.Atoi(typeParam); err == nil {
|
||||||
|
typeFilter = tp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeFilter >= 0 {
|
||||||
|
filtered := make([]*model.Channel, 0, len(channelData))
|
||||||
|
for _, ch := range channelData {
|
||||||
|
if ch.Type == typeFilter {
|
||||||
|
filtered = append(filtered, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelData = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("p", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
total := len(channelData)
|
||||||
|
startIdx := (page - 1) * pageSize
|
||||||
|
if startIdx > total {
|
||||||
|
startIdx = total
|
||||||
|
}
|
||||||
|
endIdx := startIdx + pageSize
|
||||||
|
if endIdx > total {
|
||||||
|
endIdx = total
|
||||||
|
}
|
||||||
|
|
||||||
|
pagedData := channelData[startIdx:endIdx]
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": channelData,
|
"data": gin.H{
|
||||||
|
"items": pagedData,
|
||||||
|
"total": total,
|
||||||
|
"type_counts": typeCounts,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -243,9 +390,17 @@ func AddChannel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = channel.ValidateSettings()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "channel setting 格式错误:" + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
channel.CreatedTime = common.GetTimestamp()
|
channel.CreatedTime = common.GetTimestamp()
|
||||||
keys := strings.Split(channel.Key, "\n")
|
keys := strings.Split(channel.Key, "\n")
|
||||||
if channel.Type == common.ChannelTypeVertexAi {
|
if channel.Type == constant.ChannelTypeVertexAi {
|
||||||
if channel.Other == "" {
|
if channel.Other == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -470,7 +625,15 @@ func UpdateChannel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if channel.Type == common.ChannelTypeVertexAi {
|
err = channel.ValidateSettings()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "channel setting 格式错误:" + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if channel.Type == constant.ChannelTypeVertexAi {
|
||||||
if channel.Other == "" {
|
if channel.Other == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -499,6 +662,7 @@ func UpdateChannel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
channel.Key = ""
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
@@ -524,7 +688,7 @@ func FetchModels(c *gin.Context) {
|
|||||||
|
|
||||||
baseURL := req.BaseURL
|
baseURL := req.BaseURL
|
||||||
if baseURL == "" {
|
if baseURL == "" {
|
||||||
baseURL = common.ChannelBaseURLs[req.Type]
|
baseURL = constant.ChannelBaseURLs[req.Type]
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
@@ -613,3 +777,44 @@ func BatchSetChannelTag(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetTagModels(c *gin.Context) {
|
||||||
|
tag := c.Query("tag")
|
||||||
|
if tag == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "tag不能为空",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channels, err := model.GetChannelsByTag(tag, false) // Assuming false for idSort is fine here
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var longestModels string
|
||||||
|
maxLength := 0
|
||||||
|
|
||||||
|
// Find the longest models string among all channels with the given tag
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channel.Models != "" {
|
||||||
|
currentModels := strings.Split(channel.Models, ",")
|
||||||
|
if len(currentModels) > maxLength {
|
||||||
|
maxLength = len(currentModels)
|
||||||
|
longestModels = channel.Models
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": longestModels,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
103
controller/console_migrate.go
Normal file
103
controller/console_migrate.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// 用于迁移检测的旧键,该文件下个版本会删除
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MigrateConsoleSetting 迁移旧的控制台相关配置到 console_setting.*
|
||||||
|
func MigrateConsoleSetting(c *gin.Context) {
|
||||||
|
// 读取全部 option
|
||||||
|
opts, err := model.AllOption()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 建立 map
|
||||||
|
valMap := map[string]string{}
|
||||||
|
for _, o := range opts {
|
||||||
|
valMap[o.Key] = o.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 APIInfo
|
||||||
|
if v := valMap["ApiInfo"]; v != "" {
|
||||||
|
var arr []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(v), &arr); err == nil {
|
||||||
|
if len(arr) > 50 {
|
||||||
|
arr = arr[:50]
|
||||||
|
}
|
||||||
|
bytes, _ := json.Marshal(arr)
|
||||||
|
model.UpdateOption("console_setting.api_info", string(bytes))
|
||||||
|
}
|
||||||
|
model.UpdateOption("ApiInfo", "")
|
||||||
|
}
|
||||||
|
// Announcements 直接搬
|
||||||
|
if v := valMap["Announcements"]; v != "" {
|
||||||
|
model.UpdateOption("console_setting.announcements", v)
|
||||||
|
model.UpdateOption("Announcements", "")
|
||||||
|
}
|
||||||
|
// FAQ 转换
|
||||||
|
if v := valMap["FAQ"]; v != "" {
|
||||||
|
var arr []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(v), &arr); err == nil {
|
||||||
|
out := []map[string]interface{}{}
|
||||||
|
for _, item := range arr {
|
||||||
|
q, _ := item["question"].(string)
|
||||||
|
if q == "" {
|
||||||
|
q, _ = item["title"].(string)
|
||||||
|
}
|
||||||
|
a, _ := item["answer"].(string)
|
||||||
|
if a == "" {
|
||||||
|
a, _ = item["content"].(string)
|
||||||
|
}
|
||||||
|
if q != "" && a != "" {
|
||||||
|
out = append(out, map[string]interface{}{"question": q, "answer": a})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) > 50 {
|
||||||
|
out = out[:50]
|
||||||
|
}
|
||||||
|
bytes, _ := json.Marshal(out)
|
||||||
|
model.UpdateOption("console_setting.faq", string(bytes))
|
||||||
|
}
|
||||||
|
model.UpdateOption("FAQ", "")
|
||||||
|
}
|
||||||
|
// Uptime Kuma 迁移到新的 groups 结构(console_setting.uptime_kuma_groups)
|
||||||
|
url := valMap["UptimeKumaUrl"]
|
||||||
|
slug := valMap["UptimeKumaSlug"]
|
||||||
|
if url != "" && slug != "" {
|
||||||
|
// 仅当同时存在 URL 与 Slug 时才进行迁移
|
||||||
|
groups := []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"categoryName": "old",
|
||||||
|
"url": url,
|
||||||
|
"slug": slug,
|
||||||
|
"description": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bytes, _ := json.Marshal(groups)
|
||||||
|
model.UpdateOption("console_setting.uptime_kuma_groups", string(bytes))
|
||||||
|
}
|
||||||
|
// 清空旧键内容
|
||||||
|
if url != "" {
|
||||||
|
model.UpdateOption("UptimeKumaUrl", "")
|
||||||
|
}
|
||||||
|
if slug != "" {
|
||||||
|
model.UpdateOption("UptimeKumaSlug", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除旧键记录
|
||||||
|
oldKeys := []string{"ApiInfo", "Announcements", "FAQ", "UptimeKumaUrl", "UptimeKumaSlug"}
|
||||||
|
model.DB.Where("key IN ?", oldKeys).Delete(&model.Option{})
|
||||||
|
|
||||||
|
// 重新加载 OptionMap
|
||||||
|
model.InitOptionMap()
|
||||||
|
common.SysLog("console setting migrated")
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "migrated"})
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/ratio_setting"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetGroups(c *gin.Context) {
|
func GetGroups(c *gin.Context) {
|
||||||
groupNames := make([]string, 0)
|
groupNames := make([]string, 0)
|
||||||
for groupName, _ := range setting.GetGroupRatioCopy() {
|
for groupName := range ratio_setting.GetGroupRatioCopy() {
|
||||||
groupNames = append(groupNames, groupName)
|
groupNames = append(groupNames, groupName)
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -24,7 +26,7 @@ func GetUserGroups(c *gin.Context) {
|
|||||||
userGroup := ""
|
userGroup := ""
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
userGroup, _ = model.GetUserGroup(userId, false)
|
userGroup, _ = model.GetUserGroup(userId, false)
|
||||||
for groupName, ratio := range setting.GetGroupRatioCopy() {
|
for groupName, ratio := range ratio_setting.GetGroupRatioCopy() {
|
||||||
// UserUsableGroups contains the groups that the user can use
|
// UserUsableGroups contains the groups that the user can use
|
||||||
userUsableGroups := setting.GetUserUsableGroups(userGroup)
|
userUsableGroups := setting.GetUserUsableGroups(userGroup)
|
||||||
if desc, ok := userUsableGroups[groupName]; ok {
|
if desc, ok := userUsableGroups[groupName]; ok {
|
||||||
@@ -34,6 +36,12 @@ func GetUserGroups(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if setting.GroupInUserUsableGroups("auto") {
|
||||||
|
usableGroups["auto"] = map[string]interface{}{
|
||||||
|
"ratio": "自动",
|
||||||
|
"desc": setting.GetUsableGroupDescription("auto"),
|
||||||
|
}
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|||||||
9
controller/image.go
Normal file
9
controller/image.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetImage(c *gin.Context) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -196,7 +196,7 @@ func DeleteHistoryLogs(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
count, err := model.DeleteOldLog(targetTimestamp)
|
count, err := model.DeleteOldLog(c.Request.Context(), targetTimestamp, 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
@@ -159,7 +158,7 @@ func UpdateMidjourneyTaskBulk() {
|
|||||||
common.LogError(ctx, "UpdateMidjourneyTask task error: "+err.Error())
|
common.LogError(ctx, "UpdateMidjourneyTask task error: "+err.Error())
|
||||||
} else {
|
} else {
|
||||||
if shouldReturnQuota {
|
if shouldReturnQuota {
|
||||||
err = model.IncreaseUserQuota(task.UserId, task.Quota)
|
err = model.IncreaseUserQuota(task.UserId, task.Quota, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(ctx, "fail to increase user quota: "+err.Error())
|
common.LogError(ctx, "fail to increase user quota: "+err.Error())
|
||||||
}
|
}
|
||||||
@@ -215,8 +214,12 @@ func checkMjTaskNeedUpdate(oldTask *model.Midjourney, newTask dto.MidjourneyDto)
|
|||||||
|
|
||||||
func GetAllMidjourney(c *gin.Context) {
|
func GetAllMidjourney(c *gin.Context) {
|
||||||
p, _ := strconv.Atoi(c.Query("p"))
|
p, _ := strconv.Atoi(c.Query("p"))
|
||||||
if p < 0 {
|
if p < 1 {
|
||||||
p = 0
|
p = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := strconv.Atoi(c.Query("page_size"))
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = common.ItemsPerPage
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析其他查询参数
|
// 解析其他查询参数
|
||||||
@@ -227,31 +230,38 @@ func GetAllMidjourney(c *gin.Context) {
|
|||||||
EndTimestamp: c.Query("end_timestamp"),
|
EndTimestamp: c.Query("end_timestamp"),
|
||||||
}
|
}
|
||||||
|
|
||||||
logs := model.GetAllTasks(p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
|
items := model.GetAllTasks((p-1)*pageSize, pageSize, queryParams)
|
||||||
if logs == nil {
|
total := model.CountAllTasks(queryParams)
|
||||||
logs = make([]*model.Midjourney, 0)
|
|
||||||
}
|
|
||||||
if setting.MjForwardUrlEnabled {
|
if setting.MjForwardUrlEnabled {
|
||||||
for i, midjourney := range logs {
|
for i, midjourney := range items {
|
||||||
midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
|
midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
|
||||||
logs[i] = midjourney
|
items[i] = midjourney
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": logs,
|
"data": gin.H{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"page": p,
|
||||||
|
"page_size": pageSize,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserMidjourney(c *gin.Context) {
|
func GetUserMidjourney(c *gin.Context) {
|
||||||
p, _ := strconv.Atoi(c.Query("p"))
|
p, _ := strconv.Atoi(c.Query("p"))
|
||||||
if p < 0 {
|
if p < 1 {
|
||||||
p = 0
|
p = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := strconv.Atoi(c.Query("page_size"))
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = common.ItemsPerPage
|
||||||
}
|
}
|
||||||
|
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
log.Printf("userId = %d \n", userId)
|
|
||||||
|
|
||||||
queryParams := model.TaskQueryParams{
|
queryParams := model.TaskQueryParams{
|
||||||
MjID: c.Query("mj_id"),
|
MjID: c.Query("mj_id"),
|
||||||
@@ -259,19 +269,23 @@ func GetUserMidjourney(c *gin.Context) {
|
|||||||
EndTimestamp: c.Query("end_timestamp"),
|
EndTimestamp: c.Query("end_timestamp"),
|
||||||
}
|
}
|
||||||
|
|
||||||
logs := model.GetAllUserTask(userId, p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
|
items := model.GetAllUserTask(userId, (p-1)*pageSize, pageSize, queryParams)
|
||||||
if logs == nil {
|
total := model.CountAllUserTask(userId, queryParams)
|
||||||
logs = make([]*model.Midjourney, 0)
|
|
||||||
}
|
|
||||||
if setting.MjForwardUrlEnabled {
|
if setting.MjForwardUrlEnabled {
|
||||||
for i, midjourney := range logs {
|
for i, midjourney := range items {
|
||||||
midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
|
midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
|
||||||
logs[i] = midjourney
|
items[i] = midjourney
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": logs,
|
"data": gin.H{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"page": p,
|
||||||
|
"page_size": pageSize,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
|
"one-api/middleware"
|
||||||
|
"one-api/middleware/jsrt"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/console_setting"
|
||||||
|
"one-api/setting/operation_setting"
|
||||||
|
"one-api/setting/system_setting"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -21,55 +28,85 @@ func TestStatus(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 获取HTTP统计信息
|
||||||
|
httpStats := middleware.GetStats()
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Server is running",
|
"message": "Server is running",
|
||||||
|
"http_stats": httpStats,
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetStatus(c *gin.Context) {
|
func GetStatus(c *gin.Context) {
|
||||||
|
|
||||||
|
cs := console_setting.GetConsoleSetting()
|
||||||
|
|
||||||
|
data := gin.H{
|
||||||
|
"version": common.Version,
|
||||||
|
"start_time": common.StartTime,
|
||||||
|
"email_verification": common.EmailVerificationEnabled,
|
||||||
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
|
"github_client_id": common.GitHubClientId,
|
||||||
|
"linuxdo_oauth": common.LinuxDOOAuthEnabled,
|
||||||
|
"linuxdo_client_id": common.LinuxDOClientId,
|
||||||
|
"telegram_oauth": common.TelegramOAuthEnabled,
|
||||||
|
"telegram_bot_name": common.TelegramBotName,
|
||||||
|
"system_name": common.SystemName,
|
||||||
|
"logo": common.Logo,
|
||||||
|
"footer_html": common.Footer,
|
||||||
|
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||||
|
"wechat_login": common.WeChatAuthEnabled,
|
||||||
|
"server_address": setting.ServerAddress,
|
||||||
|
"price": setting.Price,
|
||||||
|
"min_topup": setting.MinTopUp,
|
||||||
|
"turnstile_check": common.TurnstileCheckEnabled,
|
||||||
|
"turnstile_site_key": common.TurnstileSiteKey,
|
||||||
|
"top_up_link": common.TopUpLink,
|
||||||
|
"docs_link": operation_setting.GetGeneralSetting().DocsLink,
|
||||||
|
"quota_per_unit": common.QuotaPerUnit,
|
||||||
|
"display_in_currency": common.DisplayInCurrencyEnabled,
|
||||||
|
"enable_batch_update": common.BatchUpdateEnabled,
|
||||||
|
"enable_drawing": common.DrawingEnabled,
|
||||||
|
"enable_task": common.TaskEnabled,
|
||||||
|
"enable_data_export": common.DataExportEnabled,
|
||||||
|
"data_export_default_time": common.DataExportDefaultTime,
|
||||||
|
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||||
|
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
||||||
|
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||||
|
"chats": setting.Chats,
|
||||||
|
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
||||||
|
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
||||||
|
"default_use_auto_group": setting.DefaultUseAutoGroup,
|
||||||
|
"pay_methods": setting.PayMethods,
|
||||||
|
|
||||||
|
// 面板启用开关
|
||||||
|
"api_info_enabled": cs.ApiInfoEnabled,
|
||||||
|
"uptime_kuma_enabled": cs.UptimeKumaEnabled,
|
||||||
|
"announcements_enabled": cs.AnnouncementsEnabled,
|
||||||
|
"faq_enabled": cs.FAQEnabled,
|
||||||
|
|
||||||
|
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
|
||||||
|
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
|
||||||
|
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
|
||||||
|
"setup": constant.Setup,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据启用状态注入可选内容
|
||||||
|
if cs.ApiInfoEnabled {
|
||||||
|
data["api_info"] = console_setting.GetApiInfo()
|
||||||
|
}
|
||||||
|
if cs.AnnouncementsEnabled {
|
||||||
|
data["announcements"] = console_setting.GetAnnouncements()
|
||||||
|
}
|
||||||
|
if cs.FAQEnabled {
|
||||||
|
data["faq"] = console_setting.GetFAQ()
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": gin.H{
|
"data": data,
|
||||||
"version": common.Version,
|
|
||||||
"start_time": common.StartTime,
|
|
||||||
"email_verification": common.EmailVerificationEnabled,
|
|
||||||
"github_oauth": common.GitHubOAuthEnabled,
|
|
||||||
"github_client_id": common.GitHubClientId,
|
|
||||||
"linuxdo_oauth": common.LinuxDOOAuthEnabled,
|
|
||||||
"linuxdo_client_id": common.LinuxDOClientId,
|
|
||||||
"telegram_oauth": common.TelegramOAuthEnabled,
|
|
||||||
"telegram_bot_name": common.TelegramBotName,
|
|
||||||
"system_name": common.SystemName,
|
|
||||||
"logo": common.Logo,
|
|
||||||
"footer_html": common.Footer,
|
|
||||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
|
||||||
"wechat_login": common.WeChatAuthEnabled,
|
|
||||||
"server_address": setting.ServerAddress,
|
|
||||||
"price": setting.Price,
|
|
||||||
"min_topup": setting.MinTopUp,
|
|
||||||
"turnstile_check": common.TurnstileCheckEnabled,
|
|
||||||
"turnstile_site_key": common.TurnstileSiteKey,
|
|
||||||
"top_up_link": common.TopUpLink,
|
|
||||||
"chat_link": common.ChatLink,
|
|
||||||
"chat_link2": common.ChatLink2,
|
|
||||||
"quota_per_unit": common.QuotaPerUnit,
|
|
||||||
"display_in_currency": common.DisplayInCurrencyEnabled,
|
|
||||||
"enable_batch_update": common.BatchUpdateEnabled,
|
|
||||||
"enable_drawing": common.DrawingEnabled,
|
|
||||||
"enable_task": common.TaskEnabled,
|
|
||||||
"enable_data_export": common.DataExportEnabled,
|
|
||||||
"data_export_default_time": common.DataExportDefaultTime,
|
|
||||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
|
||||||
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
|
||||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
|
||||||
"chats": setting.Chats,
|
|
||||||
"demo_site_enabled": setting.DemoSiteEnabled,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetNotice(c *gin.Context) {
|
func GetNotice(c *gin.Context) {
|
||||||
@@ -80,7 +117,6 @@ func GetNotice(c *gin.Context) {
|
|||||||
"message": "",
|
"message": "",
|
||||||
"data": common.OptionMap["Notice"],
|
"data": common.OptionMap["Notice"],
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAbout(c *gin.Context) {
|
func GetAbout(c *gin.Context) {
|
||||||
@@ -91,7 +127,6 @@ func GetAbout(c *gin.Context) {
|
|||||||
"message": "",
|
"message": "",
|
||||||
"data": common.OptionMap["About"],
|
"data": common.OptionMap["About"],
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMidjourney(c *gin.Context) {
|
func GetMidjourney(c *gin.Context) {
|
||||||
@@ -102,7 +137,6 @@ func GetMidjourney(c *gin.Context) {
|
|||||||
"message": "",
|
"message": "",
|
||||||
"data": common.OptionMap["Midjourney"],
|
"data": common.OptionMap["Midjourney"],
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHomePageContent(c *gin.Context) {
|
func GetHomePageContent(c *gin.Context) {
|
||||||
@@ -113,7 +147,6 @@ func GetHomePageContent(c *gin.Context) {
|
|||||||
"message": "",
|
"message": "",
|
||||||
"data": common.OptionMap["HomePageContent"],
|
"data": common.OptionMap["HomePageContent"],
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEmailVerification(c *gin.Context) {
|
func SendEmailVerification(c *gin.Context) {
|
||||||
@@ -136,13 +169,7 @@ func SendEmailVerification(c *gin.Context) {
|
|||||||
localPart := parts[0]
|
localPart := parts[0]
|
||||||
domainPart := parts[1]
|
domainPart := parts[1]
|
||||||
if common.EmailDomainRestrictionEnabled {
|
if common.EmailDomainRestrictionEnabled {
|
||||||
allowed := false
|
allowed := slices.Contains(common.EmailDomainWhitelist, domainPart)
|
||||||
for _, domain := range common.EmailDomainWhitelist {
|
|
||||||
if domainPart == domain {
|
|
||||||
allowed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -187,7 +214,6 @@ func SendEmailVerification(c *gin.Context) {
|
|||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendPasswordResetEmail(c *gin.Context) {
|
func SendPasswordResetEmail(c *gin.Context) {
|
||||||
@@ -226,7 +252,6 @@ func SendPasswordResetEmail(c *gin.Context) {
|
|||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PasswordResetRequest struct {
|
type PasswordResetRequest struct {
|
||||||
@@ -266,5 +291,13 @@ func ResetPassword(c *gin.Context) {
|
|||||||
"message": "",
|
"message": "",
|
||||||
"data": password,
|
"data": password,
|
||||||
})
|
})
|
||||||
return
|
}
|
||||||
|
|
||||||
|
func ReloadJSScripts(c *gin.Context) {
|
||||||
|
jsrt.ReloadJSScripts()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "JavaScript 脚本已重新加载",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/samber/lo"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
@@ -14,7 +15,7 @@ import (
|
|||||||
"one-api/relay/channel/minimax"
|
"one-api/relay/channel/minimax"
|
||||||
"one-api/relay/channel/moonshot"
|
"one-api/relay/channel/moonshot"
|
||||||
relaycommon "one-api/relay/common"
|
relaycommon "one-api/relay/common"
|
||||||
relayconstant "one-api/relay/constant"
|
"one-api/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://platform.openai.com/docs/api-reference/models/list
|
// https://platform.openai.com/docs/api-reference/models/list
|
||||||
@@ -23,30 +24,10 @@ var openAIModels []dto.OpenAIModels
|
|||||||
var openAIModelsMap map[string]dto.OpenAIModels
|
var openAIModelsMap map[string]dto.OpenAIModels
|
||||||
var channelId2Models map[int][]string
|
var channelId2Models map[int][]string
|
||||||
|
|
||||||
func getPermission() []dto.OpenAIModelPermission {
|
|
||||||
var permission []dto.OpenAIModelPermission
|
|
||||||
permission = append(permission, dto.OpenAIModelPermission{
|
|
||||||
Id: "modelperm-LwHkVFn8AcMItP432fKKDIKJ",
|
|
||||||
Object: "model_permission",
|
|
||||||
Created: 1626777600,
|
|
||||||
AllowCreateEngine: true,
|
|
||||||
AllowSampling: true,
|
|
||||||
AllowLogprobs: true,
|
|
||||||
AllowSearchIndices: false,
|
|
||||||
AllowView: true,
|
|
||||||
AllowFineTuning: false,
|
|
||||||
Organization: "*",
|
|
||||||
Group: nil,
|
|
||||||
IsBlocking: false,
|
|
||||||
})
|
|
||||||
return permission
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||||
permission := getPermission()
|
for i := 0; i < constant.APITypeDummy; i++ {
|
||||||
for i := 0; i < relayconstant.APITypeDummy; i++ {
|
if i == constant.APITypeAIProxyLibrary {
|
||||||
if i == relayconstant.APITypeAIProxyLibrary {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
adaptor := relay.GetAdaptor(i)
|
adaptor := relay.GetAdaptor(i)
|
||||||
@@ -54,69 +35,51 @@ func init() {
|
|||||||
modelNames := adaptor.GetModelList()
|
modelNames := adaptor.GetModelList()
|
||||||
for _, modelName := range modelNames {
|
for _, modelName := range modelNames {
|
||||||
openAIModels = append(openAIModels, dto.OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: channelName,
|
OwnedBy: channelName,
|
||||||
Permission: permission,
|
|
||||||
Root: modelName,
|
|
||||||
Parent: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, modelName := range ai360.ModelList {
|
for _, modelName := range ai360.ModelList {
|
||||||
openAIModels = append(openAIModels, dto.OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: ai360.ChannelName,
|
OwnedBy: ai360.ChannelName,
|
||||||
Permission: permission,
|
|
||||||
Root: modelName,
|
|
||||||
Parent: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, modelName := range moonshot.ModelList {
|
for _, modelName := range moonshot.ModelList {
|
||||||
openAIModels = append(openAIModels, dto.OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: moonshot.ChannelName,
|
OwnedBy: moonshot.ChannelName,
|
||||||
Permission: permission,
|
|
||||||
Root: modelName,
|
|
||||||
Parent: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, modelName := range lingyiwanwu.ModelList {
|
for _, modelName := range lingyiwanwu.ModelList {
|
||||||
openAIModels = append(openAIModels, dto.OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: lingyiwanwu.ChannelName,
|
OwnedBy: lingyiwanwu.ChannelName,
|
||||||
Permission: permission,
|
|
||||||
Root: modelName,
|
|
||||||
Parent: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, modelName := range minimax.ModelList {
|
for _, modelName := range minimax.ModelList {
|
||||||
openAIModels = append(openAIModels, dto.OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: minimax.ChannelName,
|
OwnedBy: minimax.ChannelName,
|
||||||
Permission: permission,
|
|
||||||
Root: modelName,
|
|
||||||
Parent: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for modelName, _ := range constant.MidjourneyModel2Action {
|
for modelName, _ := range constant.MidjourneyModel2Action {
|
||||||
openAIModels = append(openAIModels, dto.OpenAIModels{
|
openAIModels = append(openAIModels, dto.OpenAIModels{
|
||||||
Id: modelName,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: "midjourney",
|
OwnedBy: "midjourney",
|
||||||
Permission: permission,
|
|
||||||
Root: modelName,
|
|
||||||
Parent: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
openAIModelsMap = make(map[string]dto.OpenAIModels)
|
openAIModelsMap = make(map[string]dto.OpenAIModels)
|
||||||
@@ -124,9 +87,9 @@ func init() {
|
|||||||
openAIModelsMap[aiModel.Id] = aiModel
|
openAIModelsMap[aiModel.Id] = aiModel
|
||||||
}
|
}
|
||||||
channelId2Models = make(map[int][]string)
|
channelId2Models = make(map[int][]string)
|
||||||
for i := 1; i <= common.ChannelTypeDummy; i++ {
|
for i := 1; i <= constant.ChannelTypeDummy; i++ {
|
||||||
apiType, success := relayconstant.ChannelType2APIType(i)
|
apiType, success := common.ChannelType2APIType(i)
|
||||||
if !success || apiType == relayconstant.APITypeAIProxyLibrary {
|
if !success || apiType == constant.APITypeAIProxyLibrary {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
meta := &relaycommon.RelayInfo{ChannelType: i}
|
meta := &relaycommon.RelayInfo{ChannelType: i}
|
||||||
@@ -134,15 +97,17 @@ func init() {
|
|||||||
adaptor.Init(meta)
|
adaptor.Init(meta)
|
||||||
channelId2Models[i] = adaptor.GetModelList()
|
channelId2Models[i] = adaptor.GetModelList()
|
||||||
}
|
}
|
||||||
|
openAIModels = lo.UniqBy(openAIModels, func(m dto.OpenAIModels) string {
|
||||||
|
return m.Id
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListModels(c *gin.Context) {
|
func ListModels(c *gin.Context) {
|
||||||
userOpenAiModels := make([]dto.OpenAIModels, 0)
|
userOpenAiModels := make([]dto.OpenAIModels, 0)
|
||||||
permission := getPermission()
|
|
||||||
|
|
||||||
modelLimitEnable := c.GetBool("token_model_limit_enabled")
|
modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled)
|
||||||
if modelLimitEnable {
|
if modelLimitEnable {
|
||||||
s, ok := c.Get("token_model_limit")
|
s, ok := common.GetContextKey(c, constant.ContextKeyTokenModelLimit)
|
||||||
var tokenModelLimit map[string]bool
|
var tokenModelLimit map[string]bool
|
||||||
if ok {
|
if ok {
|
||||||
tokenModelLimit = s.(map[string]bool)
|
tokenModelLimit = s.(map[string]bool)
|
||||||
@@ -150,23 +115,22 @@ func ListModels(c *gin.Context) {
|
|||||||
tokenModelLimit = map[string]bool{}
|
tokenModelLimit = map[string]bool{}
|
||||||
}
|
}
|
||||||
for allowModel, _ := range tokenModelLimit {
|
for allowModel, _ := range tokenModelLimit {
|
||||||
if _, ok := openAIModelsMap[allowModel]; ok {
|
if oaiModel, ok := openAIModelsMap[allowModel]; ok {
|
||||||
userOpenAiModels = append(userOpenAiModels, openAIModelsMap[allowModel])
|
oaiModel.SupportedEndpointTypes = model.GetModelSupportEndpointTypes(allowModel)
|
||||||
|
userOpenAiModels = append(userOpenAiModels, oaiModel)
|
||||||
} else {
|
} else {
|
||||||
userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
|
userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
|
||||||
Id: allowModel,
|
Id: allowModel,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: "custom",
|
OwnedBy: "custom",
|
||||||
Permission: permission,
|
SupportedEndpointTypes: model.GetModelSupportEndpointTypes(allowModel),
|
||||||
Root: allowModel,
|
|
||||||
Parent: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
userGroup, err := model.GetUserGroup(userId, true)
|
userGroup, err := model.GetUserGroup(userId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -175,23 +139,34 @@ func ListModels(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
group := userGroup
|
group := userGroup
|
||||||
tokenGroup := c.GetString("token_group")
|
tokenGroup := common.GetContextKeyString(c, constant.ContextKeyTokenGroup)
|
||||||
if tokenGroup != "" {
|
if tokenGroup != "" {
|
||||||
group = tokenGroup
|
group = tokenGroup
|
||||||
}
|
}
|
||||||
models := model.GetGroupModels(group)
|
var models []string
|
||||||
for _, s := range models {
|
if tokenGroup == "auto" {
|
||||||
if _, ok := openAIModelsMap[s]; ok {
|
for _, autoGroup := range setting.AutoGroups {
|
||||||
userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
|
groupModels := model.GetGroupEnabledModels(autoGroup)
|
||||||
|
for _, g := range groupModels {
|
||||||
|
if !common.StringsContains(models, g) {
|
||||||
|
models = append(models, g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
models = model.GetGroupEnabledModels(group)
|
||||||
|
}
|
||||||
|
for _, modelName := range models {
|
||||||
|
if oaiModel, ok := openAIModelsMap[modelName]; ok {
|
||||||
|
oaiModel.SupportedEndpointTypes = model.GetModelSupportEndpointTypes(modelName)
|
||||||
|
userOpenAiModels = append(userOpenAiModels, oaiModel)
|
||||||
} else {
|
} else {
|
||||||
userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
|
userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
|
||||||
Id: s,
|
Id: modelName,
|
||||||
Object: "model",
|
Object: "model",
|
||||||
Created: 1626777600,
|
Created: 1626777600,
|
||||||
OwnedBy: "custom",
|
OwnedBy: "custom",
|
||||||
Permission: permission,
|
SupportedEndpointTypes: model.GetModelSupportEndpointTypes(modelName),
|
||||||
Root: s,
|
|
||||||
Parent: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,6 +191,13 @@ func DashboardListModels(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EnabledListModels(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": model.GetEnabledModels(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func RetrieveModel(c *gin.Context) {
|
func RetrieveModel(c *gin.Context) {
|
||||||
modelId := c.Param("model")
|
modelId := c.Param("model")
|
||||||
if aiModel, ok := openAIModelsMap[modelId]; ok {
|
if aiModel, ok := openAIModelsMap[modelId]; ok {
|
||||||
|
|||||||
240
controller/oidc.go
Normal file
240
controller/oidc.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"one-api/setting"
|
||||||
|
"one-api/setting/system_setting"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OidcResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
IDToken string `json:"id_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcUser struct {
|
||||||
|
OpenID string `json:"sub"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PreferredUsername string `json:"preferred_username"`
|
||||||
|
Picture string `json:"picture"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOidcUserInfoByCode(code string) (*OidcUser, error) {
|
||||||
|
if code == "" {
|
||||||
|
return nil, errors.New("无效的参数")
|
||||||
|
}
|
||||||
|
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("client_id", system_setting.GetOIDCSettings().ClientId)
|
||||||
|
values.Set("client_secret", system_setting.GetOIDCSettings().ClientSecret)
|
||||||
|
values.Set("code", code)
|
||||||
|
values.Set("grant_type", "authorization_code")
|
||||||
|
values.Set("redirect_uri", fmt.Sprintf("%s/oauth/oidc", setting.ServerAddress))
|
||||||
|
formData := values.Encode()
|
||||||
|
req, err := http.NewRequest("POST", system_setting.GetOIDCSettings().TokenEndpoint, strings.NewReader(formData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
var oidcResponse OidcResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&oidcResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if oidcResponse.AccessToken == "" {
|
||||||
|
common.SysError("OIDC 获取 Token 失败,请检查设置!")
|
||||||
|
return nil, errors.New("OIDC 获取 Token 失败,请检查设置!")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = http.NewRequest("GET", system_setting.GetOIDCSettings().UserInfoEndpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+oidcResponse.AccessToken)
|
||||||
|
res2, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res2.Body.Close()
|
||||||
|
if res2.StatusCode != http.StatusOK {
|
||||||
|
common.SysError("OIDC 获取用户信息失败!请检查设置!")
|
||||||
|
return nil, errors.New("OIDC 获取用户信息失败!请检查设置!")
|
||||||
|
}
|
||||||
|
|
||||||
|
var oidcUser OidcUser
|
||||||
|
err = json.NewDecoder(res2.Body).Decode(&oidcUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if oidcUser.OpenID == "" || oidcUser.Email == "" {
|
||||||
|
common.SysError("OIDC 获取用户信息为空!请检查设置!")
|
||||||
|
return nil, errors.New("OIDC 获取用户信息为空!请检查设置!")
|
||||||
|
}
|
||||||
|
return &oidcUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OidcAuth(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
state := c.Query("state")
|
||||||
|
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "state is empty or not same",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := session.Get("username")
|
||||||
|
if username != nil {
|
||||||
|
OidcBind(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !system_setting.GetOIDCSettings().Enabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 OIDC 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
oidcUser, err := getOidcUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
OidcId: oidcUser.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsOidcIdAlreadyTaken(user.OidcId) {
|
||||||
|
err := user.FillUserByOidcId()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if common.RegisterEnabled {
|
||||||
|
user.Email = oidcUser.Email
|
||||||
|
if oidcUser.PreferredUsername != "" {
|
||||||
|
user.Username = oidcUser.PreferredUsername
|
||||||
|
} else {
|
||||||
|
user.Username = "oidc_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
}
|
||||||
|
if oidcUser.Name != "" {
|
||||||
|
user.DisplayName = oidcUser.Name
|
||||||
|
} else {
|
||||||
|
user.DisplayName = "OIDC User"
|
||||||
|
}
|
||||||
|
err := user.Insert(0)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员关闭了新用户注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != common.UserStatusEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "用户已被封禁",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OidcBind(c *gin.Context) {
|
||||||
|
if !system_setting.GetOIDCSettings().Enabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 OIDC 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
oidcUser, err := getOidcUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
OidcId: oidcUser.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsOidcIdAlreadyTaken(user.OidcId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该 OIDC 账户已被绑定",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session := sessions.Default(c)
|
||||||
|
id := session.Get("id")
|
||||||
|
// id := c.GetInt("id") // critical bug!
|
||||||
|
user.Id = id.(int)
|
||||||
|
err = user.FillUserById()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.OidcId = oidcUser.OpenID
|
||||||
|
err = user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "bind",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@ import (
|
|||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/console_setting"
|
||||||
|
"one-api/setting/ratio_setting"
|
||||||
|
"one-api/setting/system_setting"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -51,6 +54,14 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "oidc.enabled":
|
||||||
|
if option.Value == "true" && system_setting.GetOIDCSettings().ClientId == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用 OIDC 登录,请先填入 OIDC Client Id 以及 OIDC Client Secret!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
case "LinuxDOOAuthEnabled":
|
case "LinuxDOOAuthEnabled":
|
||||||
if option.Value == "true" && common.LinuxDOClientId == "" {
|
if option.Value == "true" && common.LinuxDOClientId == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -81,10 +92,64 @@ func UpdateOption(c *gin.Context) {
|
|||||||
"success": false,
|
"success": false,
|
||||||
"message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!",
|
"message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "TelegramOAuthEnabled":
|
||||||
|
if option.Value == "true" && common.TelegramBotToken == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用 Telegram OAuth,请先填入 Telegram Bot Token!",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "GroupRatio":
|
case "GroupRatio":
|
||||||
err = setting.CheckGroupRatio(option.Value)
|
err = ratio_setting.CheckGroupRatio(option.Value)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ModelRequestRateLimitGroup":
|
||||||
|
err = setting.CheckModelRequestRateLimitGroup(option.Value)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "console_setting.api_info":
|
||||||
|
err = console_setting.ValidateConsoleSettings(option.Value, "ApiInfo")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "console_setting.announcements":
|
||||||
|
err = console_setting.ValidateConsoleSettings(option.Value, "Announcements")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "console_setting.faq":
|
||||||
|
err = console_setting.ValidateConsoleSettings(option.Value, "FAQ")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "console_setting.uptime_kuma_groups":
|
||||||
|
err = console_setting.ValidateConsoleSettings(option.Value, "UptimeKumaGroups")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
@@ -13,6 +12,8 @@ import (
|
|||||||
"one-api/service"
|
"one-api/service"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Playground(c *gin.Context) {
|
func Playground(c *gin.Context) {
|
||||||
@@ -57,13 +58,22 @@ func Playground(c *gin.Context) {
|
|||||||
c.Set("group", group)
|
c.Set("group", group)
|
||||||
}
|
}
|
||||||
c.Set("token_name", "playground-"+group)
|
c.Set("token_name", "playground-"+group)
|
||||||
channel, err := model.CacheGetRandomSatisfiedChannel(group, playgroundRequest.Model, 0)
|
channel, finalGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, playgroundRequest.Model, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", group, playgroundRequest.Model)
|
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", finalGroup, playgroundRequest.Model)
|
||||||
openaiErr = service.OpenAIErrorWrapperLocal(errors.New(message), "get_playground_channel_failed", http.StatusInternalServerError)
|
openaiErr = service.OpenAIErrorWrapperLocal(errors.New(message), "get_playground_channel_failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetupContextForSelectedChannel(c, channel, playgroundRequest.Model)
|
middleware.SetupContextForSelectedChannel(c, channel, playgroundRequest.Model)
|
||||||
c.Set(constant.ContextKeyRequestStartTime, time.Now())
|
common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
|
||||||
|
|
||||||
|
// Write user context to ensure acceptUnsetRatio is available
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
userCache, err := model.GetUserCache(userId)
|
||||||
|
if err != nil {
|
||||||
|
openaiErr = service.OpenAIErrorWrapperLocal(err, "get_user_cache_failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userCache.WriteContext(c)
|
||||||
Relay(c)
|
Relay(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"one-api/common"
|
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/ratio_setting"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetPricing(c *gin.Context) {
|
func GetPricing(c *gin.Context) {
|
||||||
@@ -12,7 +13,7 @@ func GetPricing(c *gin.Context) {
|
|||||||
userId, exists := c.Get("id")
|
userId, exists := c.Get("id")
|
||||||
usableGroup := map[string]string{}
|
usableGroup := map[string]string{}
|
||||||
groupRatio := map[string]float64{}
|
groupRatio := map[string]float64{}
|
||||||
for s, f := range setting.GetGroupRatioCopy() {
|
for s, f := range ratio_setting.GetGroupRatioCopy() {
|
||||||
groupRatio[s] = f
|
groupRatio[s] = f
|
||||||
}
|
}
|
||||||
var group string
|
var group string
|
||||||
@@ -20,12 +21,18 @@ func GetPricing(c *gin.Context) {
|
|||||||
user, err := model.GetUserCache(userId.(int))
|
user, err := model.GetUserCache(userId.(int))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
group = user.Group
|
group = user.Group
|
||||||
|
for g := range groupRatio {
|
||||||
|
ratio, ok := ratio_setting.GetGroupGroupRatio(group, g)
|
||||||
|
if ok {
|
||||||
|
groupRatio[g] = ratio
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
usableGroup = setting.GetUserUsableGroups(group)
|
usableGroup = setting.GetUserUsableGroups(group)
|
||||||
// check groupRatio contains usableGroup
|
// check groupRatio contains usableGroup
|
||||||
for group := range setting.GetGroupRatioCopy() {
|
for group := range ratio_setting.GetGroupRatioCopy() {
|
||||||
if _, ok := usableGroup[group]; !ok {
|
if _, ok := usableGroup[group]; !ok {
|
||||||
delete(groupRatio, group)
|
delete(groupRatio, group)
|
||||||
}
|
}
|
||||||
@@ -40,7 +47,7 @@ func GetPricing(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ResetModelRatio(c *gin.Context) {
|
func ResetModelRatio(c *gin.Context) {
|
||||||
defaultStr := common.DefaultModelRatio2JSONString()
|
defaultStr := ratio_setting.DefaultModelRatio2JSONString()
|
||||||
err := model.UpdateOption("ModelRatio", defaultStr)
|
err := model.UpdateOption("ModelRatio", defaultStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
@@ -49,7 +56,7 @@ func ResetModelRatio(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = common.UpdateModelRatioByJSONString(defaultStr)
|
err = ratio_setting.UpdateModelRatioByJSONString(defaultStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
|||||||
24
controller/ratio_config.go
Normal file
24
controller/ratio_config.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"one-api/setting/ratio_setting"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetRatioConfig(c *gin.Context) {
|
||||||
|
if !ratio_setting.IsExposeRatioEnabled() {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "倍率配置接口未启用",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": ratio_setting.GetExposedData(),
|
||||||
|
})
|
||||||
|
}
|
||||||
474
controller/ratio_sync.go
Normal file
474
controller/ratio_sync.go
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/dto"
|
||||||
|
"one-api/model"
|
||||||
|
"one-api/setting/ratio_setting"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeoutSeconds = 10
|
||||||
|
defaultEndpoint = "/api/ratio_config"
|
||||||
|
maxConcurrentFetches = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
var ratioTypes = []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"}
|
||||||
|
|
||||||
|
type upstreamResult struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data map[string]any `json:"data,omitempty"`
|
||||||
|
Err string `json:"err,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchUpstreamRatios(c *gin.Context) {
|
||||||
|
var req dto.UpstreamRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Timeout <= 0 {
|
||||||
|
req.Timeout = defaultTimeoutSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
var upstreams []dto.UpstreamDTO
|
||||||
|
|
||||||
|
if len(req.Upstreams) > 0 {
|
||||||
|
for _, u := range req.Upstreams {
|
||||||
|
if strings.HasPrefix(u.BaseURL, "http") {
|
||||||
|
if u.Endpoint == "" {
|
||||||
|
u.Endpoint = defaultEndpoint
|
||||||
|
}
|
||||||
|
u.BaseURL = strings.TrimRight(u.BaseURL, "/")
|
||||||
|
upstreams = append(upstreams, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if len(req.ChannelIDs) > 0 {
|
||||||
|
intIds := make([]int, 0, len(req.ChannelIDs))
|
||||||
|
for _, id64 := range req.ChannelIDs {
|
||||||
|
intIds = append(intIds, int(id64))
|
||||||
|
}
|
||||||
|
dbChannels, err := model.GetChannelsByIds(intIds)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(c.Request.Context(), "failed to query channels: "+err.Error())
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询渠道失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, ch := range dbChannels {
|
||||||
|
if base := ch.GetBaseURL(); strings.HasPrefix(base, "http") {
|
||||||
|
upstreams = append(upstreams, dto.UpstreamDTO{
|
||||||
|
ID: ch.Id,
|
||||||
|
Name: ch.Name,
|
||||||
|
BaseURL: strings.TrimRight(base, "/"),
|
||||||
|
Endpoint: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(upstreams) == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": "无有效上游渠道"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
ch := make(chan upstreamResult, len(upstreams))
|
||||||
|
|
||||||
|
sem := make(chan struct{}, maxConcurrentFetches)
|
||||||
|
|
||||||
|
client := &http.Client{Transport: &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second}}
|
||||||
|
|
||||||
|
for _, chn := range upstreams {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(chItem dto.UpstreamDTO) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
sem <- struct{}{}
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
endpoint := chItem.Endpoint
|
||||||
|
if endpoint == "" {
|
||||||
|
endpoint = defaultEndpoint
|
||||||
|
} else if !strings.HasPrefix(endpoint, "/") {
|
||||||
|
endpoint = "/" + endpoint
|
||||||
|
}
|
||||||
|
fullURL := chItem.BaseURL + endpoint
|
||||||
|
|
||||||
|
uniqueName := chItem.Name
|
||||||
|
if chItem.ID != 0 {
|
||||||
|
uniqueName = fmt.Sprintf("%s(%d)", chItem.Name, chItem.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), time.Duration(req.Timeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
common.LogWarn(c.Request.Context(), "build request failed: "+err.Error())
|
||||||
|
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
common.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+err.Error())
|
||||||
|
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
common.LogWarn(c.Request.Context(), "non-200 from "+chItem.Name+": "+resp.Status)
|
||||||
|
ch <- upstreamResult{Name: uniqueName, Err: resp.Status}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 兼容两种上游接口格式:
|
||||||
|
// type1: /api/ratio_config -> data 为 map[string]any,包含 model_ratio/completion_ratio/cache_ratio/model_price
|
||||||
|
// type2: /api/pricing -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式
|
||||||
|
var body struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
common.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error())
|
||||||
|
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !body.Success {
|
||||||
|
ch <- upstreamResult{Name: uniqueName, Err: body.Message}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试按 type1 解析
|
||||||
|
var type1Data map[string]any
|
||||||
|
if err := json.Unmarshal(body.Data, &type1Data); err == nil {
|
||||||
|
// 如果包含至少一个 ratioTypes 字段,则认为是 type1
|
||||||
|
isType1 := false
|
||||||
|
for _, rt := range ratioTypes {
|
||||||
|
if _, ok := type1Data[rt]; ok {
|
||||||
|
isType1 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isType1 {
|
||||||
|
ch <- upstreamResult{Name: uniqueName, Data: type1Data}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是 type1,则尝试按 type2 (/api/pricing) 解析
|
||||||
|
var pricingItems []struct {
|
||||||
|
ModelName string `json:"model_name"`
|
||||||
|
QuotaType int `json:"quota_type"`
|
||||||
|
ModelRatio float64 `json:"model_ratio"`
|
||||||
|
ModelPrice float64 `json:"model_price"`
|
||||||
|
CompletionRatio float64 `json:"completion_ratio"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body.Data, &pricingItems); err != nil {
|
||||||
|
common.LogWarn(c.Request.Context(), "unrecognized data format from "+chItem.Name+": "+err.Error())
|
||||||
|
ch <- upstreamResult{Name: uniqueName, Err: "无法解析上游返回数据"}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modelRatioMap := make(map[string]float64)
|
||||||
|
completionRatioMap := make(map[string]float64)
|
||||||
|
modelPriceMap := make(map[string]float64)
|
||||||
|
|
||||||
|
for _, item := range pricingItems {
|
||||||
|
if item.QuotaType == 1 {
|
||||||
|
modelPriceMap[item.ModelName] = item.ModelPrice
|
||||||
|
} else {
|
||||||
|
modelRatioMap[item.ModelName] = item.ModelRatio
|
||||||
|
// completionRatio 可能为 0,此时也直接赋值,保持与上游一致
|
||||||
|
completionRatioMap[item.ModelName] = item.CompletionRatio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := make(map[string]any)
|
||||||
|
|
||||||
|
if len(modelRatioMap) > 0 {
|
||||||
|
ratioAny := make(map[string]any, len(modelRatioMap))
|
||||||
|
for k, v := range modelRatioMap {
|
||||||
|
ratioAny[k] = v
|
||||||
|
}
|
||||||
|
converted["model_ratio"] = ratioAny
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(completionRatioMap) > 0 {
|
||||||
|
compAny := make(map[string]any, len(completionRatioMap))
|
||||||
|
for k, v := range completionRatioMap {
|
||||||
|
compAny[k] = v
|
||||||
|
}
|
||||||
|
converted["completion_ratio"] = compAny
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(modelPriceMap) > 0 {
|
||||||
|
priceAny := make(map[string]any, len(modelPriceMap))
|
||||||
|
for k, v := range modelPriceMap {
|
||||||
|
priceAny[k] = v
|
||||||
|
}
|
||||||
|
converted["model_price"] = priceAny
|
||||||
|
}
|
||||||
|
|
||||||
|
ch <- upstreamResult{Name: uniqueName, Data: converted}
|
||||||
|
}(chn)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
|
||||||
|
localData := ratio_setting.GetExposedData()
|
||||||
|
|
||||||
|
var testResults []dto.TestResult
|
||||||
|
var successfulChannels []struct {
|
||||||
|
name string
|
||||||
|
data map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
for r := range ch {
|
||||||
|
if r.Err != "" {
|
||||||
|
testResults = append(testResults, dto.TestResult{
|
||||||
|
Name: r.Name,
|
||||||
|
Status: "error",
|
||||||
|
Error: r.Err,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
testResults = append(testResults, dto.TestResult{
|
||||||
|
Name: r.Name,
|
||||||
|
Status: "success",
|
||||||
|
})
|
||||||
|
successfulChannels = append(successfulChannels, struct {
|
||||||
|
name string
|
||||||
|
data map[string]any
|
||||||
|
}{name: r.Name, data: r.Data})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
differences := buildDifferences(localData, successfulChannels)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": gin.H{
|
||||||
|
"differences": differences,
|
||||||
|
"test_results": testResults,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDifferences(localData map[string]any, successfulChannels []struct {
|
||||||
|
name string
|
||||||
|
data map[string]any
|
||||||
|
}) map[string]map[string]dto.DifferenceItem {
|
||||||
|
differences := make(map[string]map[string]dto.DifferenceItem)
|
||||||
|
|
||||||
|
allModels := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, ratioType := range ratioTypes {
|
||||||
|
if localRatioAny, ok := localData[ratioType]; ok {
|
||||||
|
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
||||||
|
for modelName := range localRatio {
|
||||||
|
allModels[modelName] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range successfulChannels {
|
||||||
|
for _, ratioType := range ratioTypes {
|
||||||
|
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
||||||
|
for modelName := range upstreamRatio {
|
||||||
|
allModels[modelName] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
confidenceMap := make(map[string]map[string]bool)
|
||||||
|
|
||||||
|
// 预处理阶段:检查pricing接口的可信度
|
||||||
|
for _, channel := range successfulChannels {
|
||||||
|
confidenceMap[channel.name] = make(map[string]bool)
|
||||||
|
|
||||||
|
modelRatios, hasModelRatio := channel.data["model_ratio"].(map[string]any)
|
||||||
|
completionRatios, hasCompletionRatio := channel.data["completion_ratio"].(map[string]any)
|
||||||
|
|
||||||
|
if hasModelRatio && hasCompletionRatio {
|
||||||
|
// 遍历所有模型,检查是否满足不可信条件
|
||||||
|
for modelName := range allModels {
|
||||||
|
// 默认为可信
|
||||||
|
confidenceMap[channel.name][modelName] = true
|
||||||
|
|
||||||
|
// 检查是否满足不可信条件:model_ratio为37.5且completion_ratio为1
|
||||||
|
if modelRatioVal, ok := modelRatios[modelName]; ok {
|
||||||
|
if completionRatioVal, ok := completionRatios[modelName]; ok {
|
||||||
|
// 转换为float64进行比较
|
||||||
|
if modelRatioFloat, ok := modelRatioVal.(float64); ok {
|
||||||
|
if completionRatioFloat, ok := completionRatioVal.(float64); ok {
|
||||||
|
if modelRatioFloat == 37.5 && completionRatioFloat == 1.0 {
|
||||||
|
confidenceMap[channel.name][modelName] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果不是从pricing接口获取的数据,则全部标记为可信
|
||||||
|
for modelName := range allModels {
|
||||||
|
confidenceMap[channel.name][modelName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for modelName := range allModels {
|
||||||
|
for _, ratioType := range ratioTypes {
|
||||||
|
var localValue interface{} = nil
|
||||||
|
if localRatioAny, ok := localData[ratioType]; ok {
|
||||||
|
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
||||||
|
if val, exists := localRatio[modelName]; exists {
|
||||||
|
localValue = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upstreamValues := make(map[string]interface{})
|
||||||
|
confidenceValues := make(map[string]bool)
|
||||||
|
hasUpstreamValue := false
|
||||||
|
hasDifference := false
|
||||||
|
|
||||||
|
for _, channel := range successfulChannels {
|
||||||
|
var upstreamValue interface{} = nil
|
||||||
|
|
||||||
|
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
||||||
|
if val, exists := upstreamRatio[modelName]; exists {
|
||||||
|
upstreamValue = val
|
||||||
|
hasUpstreamValue = true
|
||||||
|
|
||||||
|
if localValue != nil && localValue != val {
|
||||||
|
hasDifference = true
|
||||||
|
} else if localValue == val {
|
||||||
|
upstreamValue = "same"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if upstreamValue == nil && localValue == nil {
|
||||||
|
upstreamValue = "same"
|
||||||
|
}
|
||||||
|
|
||||||
|
if localValue == nil && upstreamValue != nil && upstreamValue != "same" {
|
||||||
|
hasDifference = true
|
||||||
|
}
|
||||||
|
|
||||||
|
upstreamValues[channel.name] = upstreamValue
|
||||||
|
|
||||||
|
confidenceValues[channel.name] = confidenceMap[channel.name][modelName]
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldInclude := false
|
||||||
|
|
||||||
|
if localValue != nil {
|
||||||
|
if hasDifference {
|
||||||
|
shouldInclude = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if hasUpstreamValue {
|
||||||
|
shouldInclude = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldInclude {
|
||||||
|
if differences[modelName] == nil {
|
||||||
|
differences[modelName] = make(map[string]dto.DifferenceItem)
|
||||||
|
}
|
||||||
|
differences[modelName][ratioType] = dto.DifferenceItem{
|
||||||
|
Current: localValue,
|
||||||
|
Upstreams: upstreamValues,
|
||||||
|
Confidence: confidenceValues,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channelHasDiff := make(map[string]bool)
|
||||||
|
for _, ratioMap := range differences {
|
||||||
|
for _, item := range ratioMap {
|
||||||
|
for chName, val := range item.Upstreams {
|
||||||
|
if val != nil && val != "same" {
|
||||||
|
channelHasDiff[chName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for modelName, ratioMap := range differences {
|
||||||
|
for ratioType, item := range ratioMap {
|
||||||
|
for chName := range item.Upstreams {
|
||||||
|
if !channelHasDiff[chName] {
|
||||||
|
delete(item.Upstreams, chName)
|
||||||
|
delete(item.Confidence, chName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allSame := true
|
||||||
|
for _, v := range item.Upstreams {
|
||||||
|
if v != "same" {
|
||||||
|
allSame = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(item.Upstreams) == 0 || allSame {
|
||||||
|
delete(ratioMap, ratioType)
|
||||||
|
} else {
|
||||||
|
differences[modelName][ratioType] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ratioMap) == 0 {
|
||||||
|
delete(differences, modelName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return differences
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSyncableChannels(c *gin.Context) {
|
||||||
|
channels, err := model.GetAllChannels(0, 0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncableChannels []dto.SyncableChannel
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channel.GetBaseURL() != "" {
|
||||||
|
syncableChannels = append(syncableChannels, dto.SyncableChannel{
|
||||||
|
ID: channel.Id,
|
||||||
|
Name: channel.Name,
|
||||||
|
BaseURL: channel.GetBaseURL(),
|
||||||
|
Status: channel.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": syncableChannels,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -126,6 +127,10 @@ func AddRedemption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := validateExpiredTime(redemption.ExpiredTime); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
var keys []string
|
var keys []string
|
||||||
for i := 0; i < redemption.Count; i++ {
|
for i := 0; i < redemption.Count; i++ {
|
||||||
key := common.GetUUID()
|
key := common.GetUUID()
|
||||||
@@ -135,6 +140,7 @@ func AddRedemption(c *gin.Context) {
|
|||||||
Key: key,
|
Key: key,
|
||||||
CreatedTime: common.GetTimestamp(),
|
CreatedTime: common.GetTimestamp(),
|
||||||
Quota: redemption.Quota,
|
Quota: redemption.Quota,
|
||||||
|
ExpiredTime: redemption.ExpiredTime,
|
||||||
}
|
}
|
||||||
err = cleanRedemption.Insert()
|
err = cleanRedemption.Insert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -191,12 +197,18 @@ func UpdateRedemption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if statusOnly != "" {
|
if statusOnly == "" {
|
||||||
cleanRedemption.Status = redemption.Status
|
if err := validateExpiredTime(redemption.ExpiredTime); err != nil {
|
||||||
} else {
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
// If you add more fields, please also update redemption.Update()
|
// If you add more fields, please also update redemption.Update()
|
||||||
cleanRedemption.Name = redemption.Name
|
cleanRedemption.Name = redemption.Name
|
||||||
cleanRedemption.Quota = redemption.Quota
|
cleanRedemption.Quota = redemption.Quota
|
||||||
|
cleanRedemption.ExpiredTime = redemption.ExpiredTime
|
||||||
|
}
|
||||||
|
if statusOnly != "" {
|
||||||
|
cleanRedemption.Status = redemption.Status
|
||||||
}
|
}
|
||||||
err = cleanRedemption.Update()
|
err = cleanRedemption.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -213,3 +225,27 @@ func UpdateRedemption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteInvalidRedemption(c *gin.Context) {
|
||||||
|
rows, err := model.DeleteInvalidRedemptions()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": rows,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateExpiredTime(expired int64) error {
|
||||||
|
if expired != 0 && expired < common.GetTimestamp() {
|
||||||
|
return errors.New("过期时间不能早于当前时间")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,26 +4,29 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
|
constant2 "one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/middleware"
|
"one-api/middleware"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/relay"
|
"one-api/relay"
|
||||||
"one-api/relay/constant"
|
|
||||||
relayconstant "one-api/relay/constant"
|
relayconstant "one-api/relay/constant"
|
||||||
|
"one-api/relay/helper"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode {
|
func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode {
|
||||||
var err *dto.OpenAIErrorWithStatusCode
|
var err *dto.OpenAIErrorWithStatusCode
|
||||||
switch relayMode {
|
switch relayMode {
|
||||||
case relayconstant.RelayModeImagesGenerations:
|
case relayconstant.RelayModeImagesGenerations, relayconstant.RelayModeImagesEdits:
|
||||||
err = relay.ImageHelper(c)
|
err = relay.ImageHelper(c)
|
||||||
case relayconstant.RelayModeAudioSpeech:
|
case relayconstant.RelayModeAudioSpeech:
|
||||||
fallthrough
|
fallthrough
|
||||||
@@ -35,23 +38,38 @@ func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
|
|||||||
err = relay.RerankHelper(c, relayMode)
|
err = relay.RerankHelper(c, relayMode)
|
||||||
case relayconstant.RelayModeEmbeddings:
|
case relayconstant.RelayModeEmbeddings:
|
||||||
err = relay.EmbeddingHelper(c)
|
err = relay.EmbeddingHelper(c)
|
||||||
|
case relayconstant.RelayModeResponses:
|
||||||
|
err = relay.ResponsesHelper(c)
|
||||||
|
case relayconstant.RelayModeGemini:
|
||||||
|
err = relay.GeminiHelper(c)
|
||||||
default:
|
default:
|
||||||
err = relay.TextHelper(c)
|
err = relay.TextHelper(c)
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsHandler(c *gin.Context, ws *websocket.Conn, relayMode int) *dto.OpenAIErrorWithStatusCode {
|
if constant2.ErrorLogEnabled && err != nil {
|
||||||
var err *dto.OpenAIErrorWithStatusCode
|
// 保存错误日志到mysql中
|
||||||
switch relayMode {
|
userId := c.GetInt("id")
|
||||||
default:
|
tokenName := c.GetString("token_name")
|
||||||
err = relay.TextHelper(c)
|
modelName := c.GetString("original_model")
|
||||||
|
tokenId := c.GetInt("token_id")
|
||||||
|
userGroup := c.GetString("group")
|
||||||
|
channelId := c.GetInt("channel_id")
|
||||||
|
other := make(map[string]interface{})
|
||||||
|
other["error_type"] = err.Error.Type
|
||||||
|
other["error_code"] = err.Error.Code
|
||||||
|
other["status_code"] = err.StatusCode
|
||||||
|
other["channel_id"] = channelId
|
||||||
|
other["channel_name"] = c.GetString("channel_name")
|
||||||
|
other["channel_type"] = c.GetInt("channel_type")
|
||||||
|
|
||||||
|
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.Error.Message, tokenId, 0, false, userGroup, other)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Relay(c *gin.Context) {
|
func Relay(c *gin.Context) {
|
||||||
relayMode := constant.Path2RelayMode(c.Request.URL.Path)
|
relayMode := relayconstant.Path2RelayMode(c.Request.URL.Path)
|
||||||
requestId := c.GetString(common.RequestIdKey)
|
requestId := c.GetString(common.RequestIdKey)
|
||||||
group := c.GetString("group")
|
group := c.GetString("group")
|
||||||
originalModel := c.GetString("original_model")
|
originalModel := c.GetString("original_model")
|
||||||
@@ -85,6 +103,7 @@ func Relay(c *gin.Context) {
|
|||||||
|
|
||||||
if openaiErr != nil {
|
if openaiErr != nil {
|
||||||
if openaiErr.StatusCode == http.StatusTooManyRequests {
|
if openaiErr.StatusCode == http.StatusTooManyRequests {
|
||||||
|
common.LogError(c, fmt.Sprintf("origin 429 error: %s", openaiErr.Error.Message))
|
||||||
openaiErr.Error.Message = "当前分组上游负载已饱和,请稍后再试"
|
openaiErr.Error.Message = "当前分组上游负载已饱和,请稍后再试"
|
||||||
}
|
}
|
||||||
openaiErr.Error.Message = common.MessageWithRequestId(openaiErr.Error.Message, requestId)
|
openaiErr.Error.Message = common.MessageWithRequestId(openaiErr.Error.Message, requestId)
|
||||||
@@ -109,11 +128,11 @@ func WssRelay(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
openaiErr := service.OpenAIErrorWrapper(err, "get_channel_failed", http.StatusInternalServerError)
|
openaiErr := service.OpenAIErrorWrapper(err, "get_channel_failed", http.StatusInternalServerError)
|
||||||
service.WssError(c, ws, openaiErr.Error)
|
helper.WssError(c, ws, openaiErr.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
relayMode := constant.Path2RelayMode(c.Request.URL.Path)
|
relayMode := relayconstant.Path2RelayMode(c.Request.URL.Path)
|
||||||
requestId := c.GetString(common.RequestIdKey)
|
requestId := c.GetString(common.RequestIdKey)
|
||||||
group := c.GetString("group")
|
group := c.GetString("group")
|
||||||
//wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01
|
//wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01
|
||||||
@@ -151,7 +170,51 @@ func WssRelay(c *gin.Context) {
|
|||||||
openaiErr.Error.Message = "当前分组上游负载已饱和,请稍后再试"
|
openaiErr.Error.Message = "当前分组上游负载已饱和,请稍后再试"
|
||||||
}
|
}
|
||||||
openaiErr.Error.Message = common.MessageWithRequestId(openaiErr.Error.Message, requestId)
|
openaiErr.Error.Message = common.MessageWithRequestId(openaiErr.Error.Message, requestId)
|
||||||
service.WssError(c, ws, openaiErr.Error)
|
helper.WssError(c, ws, openaiErr.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RelayClaude(c *gin.Context) {
|
||||||
|
//relayMode := constant.Path2RelayMode(c.Request.URL.Path)
|
||||||
|
requestId := c.GetString(common.RequestIdKey)
|
||||||
|
group := c.GetString("group")
|
||||||
|
originalModel := c.GetString("original_model")
|
||||||
|
var claudeErr *dto.ClaudeErrorWithStatusCode
|
||||||
|
|
||||||
|
for i := 0; i <= common.RetryTimes; i++ {
|
||||||
|
channel, err := getChannel(c, group, originalModel, i)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(c, err.Error())
|
||||||
|
claudeErr = service.ClaudeErrorWrapperLocal(err, "get_channel_failed", http.StatusInternalServerError)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
claudeErr = claudeRequest(c, channel)
|
||||||
|
|
||||||
|
if claudeErr == nil {
|
||||||
|
return // 成功处理请求,直接返回
|
||||||
|
}
|
||||||
|
|
||||||
|
openaiErr := service.ClaudeErrorToOpenAIError(claudeErr)
|
||||||
|
|
||||||
|
go processChannelError(c, channel.Id, channel.Type, channel.Name, channel.GetAutoBan(), openaiErr)
|
||||||
|
|
||||||
|
if !shouldRetry(c, openaiErr, common.RetryTimes-i) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useChannel := c.GetStringSlice("use_channel")
|
||||||
|
if len(useChannel) > 1 {
|
||||||
|
retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(useChannel)), "->"), "[]"))
|
||||||
|
common.LogInfo(c, retryLogStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if claudeErr != nil {
|
||||||
|
claudeErr.Error.Message = common.MessageWithRequestId(claudeErr.Error.Message, requestId)
|
||||||
|
c.JSON(claudeErr.StatusCode, gin.H{
|
||||||
|
"type": "error",
|
||||||
|
"error": claudeErr.Error,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +232,13 @@ func wssRequest(c *gin.Context, ws *websocket.Conn, relayMode int, channel *mode
|
|||||||
return relay.WssHelper(c, ws)
|
return relay.WssHelper(c, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func claudeRequest(c *gin.Context, channel *model.Channel) *dto.ClaudeErrorWithStatusCode {
|
||||||
|
addUsedChannel(c, channel.Id)
|
||||||
|
requestBody, _ := common.GetRequestBody(c)
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||||
|
return relay.ClaudeHelper(c)
|
||||||
|
}
|
||||||
|
|
||||||
func addUsedChannel(c *gin.Context, channelId int) {
|
func addUsedChannel(c *gin.Context, channelId int) {
|
||||||
useChannel := c.GetStringSlice("use_channel")
|
useChannel := c.GetStringSlice("use_channel")
|
||||||
useChannel = append(useChannel, fmt.Sprintf("%d", channelId))
|
useChannel = append(useChannel, fmt.Sprintf("%d", channelId))
|
||||||
@@ -189,7 +259,7 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
|
|||||||
AutoBan: &autoBanInt,
|
AutoBan: &autoBanInt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
channel, err := model.CacheGetRandomSatisfiedChannel(group, originalModel, retryCount)
|
channel, _, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(fmt.Sprintf("获取重试渠道失败: %s", err.Error()))
|
return nil, errors.New(fmt.Sprintf("获取重试渠道失败: %s", err.Error()))
|
||||||
}
|
}
|
||||||
@@ -225,7 +295,7 @@ func shouldRetry(c *gin.Context, openaiErr *dto.OpenAIErrorWithStatusCode, retry
|
|||||||
}
|
}
|
||||||
if openaiErr.StatusCode == http.StatusBadRequest {
|
if openaiErr.StatusCode == http.StatusBadRequest {
|
||||||
channelType := c.GetInt("channel_type")
|
channelType := c.GetInt("channel_type")
|
||||||
if channelType == common.ChannelTypeAnthropic {
|
if channelType == constant.ChannelTypeAnthropic {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -318,7 +388,7 @@ func RelayTask(c *gin.Context) {
|
|||||||
retryTimes = 0
|
retryTimes = 0
|
||||||
}
|
}
|
||||||
for i := 0; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && i < retryTimes; i++ {
|
for i := 0; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && i < retryTimes; i++ {
|
||||||
channel, err := model.CacheGetRandomSatisfiedChannel(group, originalModel, i)
|
channel, _, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", err.Error()))
|
common.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", err.Error()))
|
||||||
break
|
break
|
||||||
@@ -350,7 +420,7 @@ func RelayTask(c *gin.Context) {
|
|||||||
func taskRelayHandler(c *gin.Context, relayMode int) *dto.TaskError {
|
func taskRelayHandler(c *gin.Context, relayMode int) *dto.TaskError {
|
||||||
var err *dto.TaskError
|
var err *dto.TaskError
|
||||||
switch relayMode {
|
switch relayMode {
|
||||||
case relayconstant.RelayModeSunoFetch, relayconstant.RelayModeSunoFetchByID:
|
case relayconstant.RelayModeSunoFetch, relayconstant.RelayModeSunoFetchByID, relayconstant.RelayModeKlingFetchByID:
|
||||||
err = relay.RelayTaskFetch(c, relayMode)
|
err = relay.RelayTaskFetch(c, relayMode)
|
||||||
default:
|
default:
|
||||||
err = relay.RelayTaskSubmit(c, relayMode)
|
err = relay.RelayTaskSubmit(c, relayMode)
|
||||||
|
|||||||
181
controller/setup.go
Normal file
181
controller/setup.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
|
"one-api/model"
|
||||||
|
"one-api/setting/operation_setting"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Setup struct {
|
||||||
|
Status bool `json:"status"`
|
||||||
|
RootInit bool `json:"root_init"`
|
||||||
|
DatabaseType string `json:"database_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetupRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
ConfirmPassword string `json:"confirmPassword"`
|
||||||
|
SelfUseModeEnabled bool `json:"SelfUseModeEnabled"`
|
||||||
|
DemoSiteEnabled bool `json:"DemoSiteEnabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSetup(c *gin.Context) {
|
||||||
|
setup := Setup{
|
||||||
|
Status: constant.Setup,
|
||||||
|
}
|
||||||
|
if constant.Setup {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": setup,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setup.RootInit = model.RootUserExists()
|
||||||
|
if common.UsingMySQL {
|
||||||
|
setup.DatabaseType = "mysql"
|
||||||
|
}
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
setup.DatabaseType = "postgres"
|
||||||
|
}
|
||||||
|
if common.UsingSQLite {
|
||||||
|
setup.DatabaseType = "sqlite"
|
||||||
|
}
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": setup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostSetup(c *gin.Context) {
|
||||||
|
// Check if setup is already completed
|
||||||
|
if constant.Setup {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "系统已经初始化完成",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if root user already exists
|
||||||
|
rootExists := model.RootUserExists()
|
||||||
|
|
||||||
|
var req SetupRequest
|
||||||
|
err := c.ShouldBindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "请求参数有误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If root doesn't exist, validate and create admin account
|
||||||
|
if !rootExists {
|
||||||
|
// Validate username length: max 12 characters to align with model.User validation
|
||||||
|
if len(req.Username) > 12 {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "用户名长度不能超过12个字符",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Validate password
|
||||||
|
if req.Password != req.ConfirmPassword {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "两次输入的密码不一致",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Password) < 8 {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "密码长度至少为8个字符",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create root user
|
||||||
|
hashedPassword, err := common.Password2Hash(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "系统错误: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rootUser := model.User{
|
||||||
|
Username: req.Username,
|
||||||
|
Password: hashedPassword,
|
||||||
|
Role: common.RoleRootUser,
|
||||||
|
Status: common.UserStatusEnabled,
|
||||||
|
DisplayName: "Root User",
|
||||||
|
AccessToken: nil,
|
||||||
|
Quota: 100000000,
|
||||||
|
}
|
||||||
|
err = model.DB.Create(&rootUser).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "创建管理员账号失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set operation modes
|
||||||
|
operation_setting.SelfUseModeEnabled = req.SelfUseModeEnabled
|
||||||
|
operation_setting.DemoSiteEnabled = req.DemoSiteEnabled
|
||||||
|
|
||||||
|
// Save operation modes to database for persistence
|
||||||
|
err = model.UpdateOption("SelfUseModeEnabled", boolToString(req.SelfUseModeEnabled))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "保存自用模式设置失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.UpdateOption("DemoSiteEnabled", boolToString(req.DemoSiteEnabled))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "保存演示站点模式设置失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update setup status
|
||||||
|
constant.Setup = true
|
||||||
|
|
||||||
|
setup := model.Setup{
|
||||||
|
Version: common.Version,
|
||||||
|
InitializedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
err = model.DB.Create(&setup).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "系统初始化失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "系统初始化成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToString(b bool) string {
|
||||||
|
if b {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
@@ -74,6 +74,8 @@ func UpdateTaskByPlatform(platform constant.TaskPlatform, taskChannelM map[int][
|
|||||||
//_ = UpdateMidjourneyTaskAll(context.Background(), tasks)
|
//_ = UpdateMidjourneyTaskAll(context.Background(), tasks)
|
||||||
case constant.TaskPlatformSuno:
|
case constant.TaskPlatformSuno:
|
||||||
_ = UpdateSunoTaskAll(context.Background(), taskChannelM, taskM)
|
_ = UpdateSunoTaskAll(context.Background(), taskChannelM, taskM)
|
||||||
|
case constant.TaskPlatformKling, constant.TaskPlatformJimeng:
|
||||||
|
_ = UpdateVideoTaskAll(context.Background(), platform, taskChannelM, taskM)
|
||||||
default:
|
default:
|
||||||
common.SysLog("未知平台")
|
common.SysLog("未知平台")
|
||||||
}
|
}
|
||||||
@@ -120,7 +122,7 @@ func updateSunoTaskAll(ctx context.Context, channelId int, taskIds []string, tas
|
|||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
common.LogError(ctx, fmt.Sprintf("Get Task status code: %d", resp.StatusCode))
|
common.LogError(ctx, fmt.Sprintf("Get Task status code: %d", resp.StatusCode))
|
||||||
return errors.New(fmt.Sprintf("Get Task status code: %d", resp.StatusCode))
|
return fmt.Errorf("Get Task status code: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
@@ -159,7 +161,7 @@ func updateSunoTaskAll(ctx context.Context, channelId int, taskIds []string, tas
|
|||||||
} else {
|
} else {
|
||||||
quota := task.Quota
|
quota := task.Quota
|
||||||
if quota != 0 {
|
if quota != 0 {
|
||||||
err = model.IncreaseUserQuota(task.UserId, quota)
|
err = model.IncreaseUserQuota(task.UserId, quota, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(ctx, "fail to increase user quota: "+err.Error())
|
common.LogError(ctx, "fail to increase user quota: "+err.Error())
|
||||||
}
|
}
|
||||||
@@ -224,9 +226,14 @@ func checkTaskNeedUpdate(oldTask *model.Task, newTask dto.SunoDataResponse) bool
|
|||||||
|
|
||||||
func GetAllTask(c *gin.Context) {
|
func GetAllTask(c *gin.Context) {
|
||||||
p, _ := strconv.Atoi(c.Query("p"))
|
p, _ := strconv.Atoi(c.Query("p"))
|
||||||
if p < 0 {
|
if p < 1 {
|
||||||
p = 0
|
p = 1
|
||||||
}
|
}
|
||||||
|
pageSize, _ := strconv.Atoi(c.Query("page_size"))
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = common.ItemsPerPage
|
||||||
|
}
|
||||||
|
|
||||||
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
||||||
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
||||||
// 解析其他查询参数
|
// 解析其他查询参数
|
||||||
@@ -237,24 +244,32 @@ func GetAllTask(c *gin.Context) {
|
|||||||
Action: c.Query("action"),
|
Action: c.Query("action"),
|
||||||
StartTimestamp: startTimestamp,
|
StartTimestamp: startTimestamp,
|
||||||
EndTimestamp: endTimestamp,
|
EndTimestamp: endTimestamp,
|
||||||
|
ChannelID: c.Query("channel_id"),
|
||||||
}
|
}
|
||||||
|
|
||||||
logs := model.TaskGetAllTasks(p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
|
items := model.TaskGetAllTasks((p-1)*pageSize, pageSize, queryParams)
|
||||||
if logs == nil {
|
total := model.TaskCountAllTasks(queryParams)
|
||||||
logs = make([]*model.Task, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": logs,
|
"data": gin.H{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"page": p,
|
||||||
|
"page_size": pageSize,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserTask(c *gin.Context) {
|
func GetUserTask(c *gin.Context) {
|
||||||
p, _ := strconv.Atoi(c.Query("p"))
|
p, _ := strconv.Atoi(c.Query("p"))
|
||||||
if p < 0 {
|
if p < 1 {
|
||||||
p = 0
|
p = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := strconv.Atoi(c.Query("page_size"))
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = common.ItemsPerPage
|
||||||
}
|
}
|
||||||
|
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
@@ -271,14 +286,17 @@ func GetUserTask(c *gin.Context) {
|
|||||||
EndTimestamp: endTimestamp,
|
EndTimestamp: endTimestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
logs := model.TaskGetAllUserTask(userId, p*common.ItemsPerPage, common.ItemsPerPage, queryParams)
|
items := model.TaskGetAllUserTask(userId, (p-1)*pageSize, pageSize, queryParams)
|
||||||
if logs == nil {
|
total := model.TaskCountAllUserTask(userId, queryParams)
|
||||||
logs = make([]*model.Task, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": logs,
|
"data": gin.H{
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"page": p,
|
||||||
|
"page_size": pageSize,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
138
controller/task_video.go
Normal file
138
controller/task_video.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
|
"one-api/model"
|
||||||
|
"one-api/relay"
|
||||||
|
"one-api/relay/channel"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, taskChannelM map[int][]string, taskM map[string]*model.Task) error {
|
||||||
|
for channelId, taskIds := range taskChannelM {
|
||||||
|
if err := updateVideoTaskAll(ctx, platform, channelId, taskIds, taskM); err != nil {
|
||||||
|
common.LogError(ctx, fmt.Sprintf("Channel #%d failed to update video async tasks: %s", channelId, err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, channelId int, taskIds []string, taskM map[string]*model.Task) error {
|
||||||
|
common.LogInfo(ctx, fmt.Sprintf("Channel #%d pending video tasks: %d", channelId, len(taskIds)))
|
||||||
|
if len(taskIds) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cacheGetChannel, err := model.CacheGetChannel(channelId)
|
||||||
|
if err != nil {
|
||||||
|
errUpdate := model.TaskBulkUpdate(taskIds, map[string]any{
|
||||||
|
"fail_reason": fmt.Sprintf("Failed to get channel info, channel ID: %d", channelId),
|
||||||
|
"status": "FAILURE",
|
||||||
|
"progress": "100%",
|
||||||
|
})
|
||||||
|
if errUpdate != nil {
|
||||||
|
common.SysError(fmt.Sprintf("UpdateVideoTask error: %v", errUpdate))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("CacheGetChannel failed: %w", err)
|
||||||
|
}
|
||||||
|
adaptor := relay.GetTaskAdaptor(platform)
|
||||||
|
if adaptor == nil {
|
||||||
|
return fmt.Errorf("video adaptor not found")
|
||||||
|
}
|
||||||
|
for _, taskId := range taskIds {
|
||||||
|
if err := updateVideoSingleTask(ctx, adaptor, cacheGetChannel, taskId, taskM); err != nil {
|
||||||
|
common.LogError(ctx, fmt.Sprintf("Failed to update video task %s: %s", taskId, err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, channel *model.Channel, taskId string, taskM map[string]*model.Task) error {
|
||||||
|
baseURL := constant.ChannelBaseURLs[channel.Type]
|
||||||
|
if channel.GetBaseURL() != "" {
|
||||||
|
baseURL = channel.GetBaseURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
task := taskM[taskId]
|
||||||
|
if task == nil {
|
||||||
|
common.LogError(ctx, fmt.Sprintf("Task %s not found in taskM", taskId))
|
||||||
|
return fmt.Errorf("task %s not found", taskId)
|
||||||
|
}
|
||||||
|
resp, err := adaptor.FetchTask(baseURL, channel.Key, map[string]any{
|
||||||
|
"task_id": taskId,
|
||||||
|
"action": task.Action,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetchTask failed for task %s: %w", taskId, err)
|
||||||
|
}
|
||||||
|
//if resp.StatusCode != http.StatusOK {
|
||||||
|
//return fmt.Errorf("get Video Task status code: %d", resp.StatusCode)
|
||||||
|
//}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("readAll failed for task %s: %w", taskId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskResult, err := adaptor.ParseTaskResult(responseBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parseTaskResult failed for task %s: %w", taskId, err)
|
||||||
|
}
|
||||||
|
//if taskResult.Code != 0 {
|
||||||
|
// return fmt.Errorf("video task fetch failed for task %s", taskId)
|
||||||
|
//}
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
if taskResult.Status == "" {
|
||||||
|
return fmt.Errorf("task %s status is empty", taskId)
|
||||||
|
}
|
||||||
|
task.Status = model.TaskStatus(taskResult.Status)
|
||||||
|
switch taskResult.Status {
|
||||||
|
case model.TaskStatusSubmitted:
|
||||||
|
task.Progress = "10%"
|
||||||
|
case model.TaskStatusQueued:
|
||||||
|
task.Progress = "20%"
|
||||||
|
case model.TaskStatusInProgress:
|
||||||
|
task.Progress = "30%"
|
||||||
|
if task.StartTime == 0 {
|
||||||
|
task.StartTime = now
|
||||||
|
}
|
||||||
|
case model.TaskStatusSuccess:
|
||||||
|
task.Progress = "100%"
|
||||||
|
if task.FinishTime == 0 {
|
||||||
|
task.FinishTime = now
|
||||||
|
}
|
||||||
|
task.FailReason = taskResult.Url
|
||||||
|
case model.TaskStatusFailure:
|
||||||
|
task.Status = model.TaskStatusFailure
|
||||||
|
task.Progress = "100%"
|
||||||
|
if task.FinishTime == 0 {
|
||||||
|
task.FinishTime = now
|
||||||
|
}
|
||||||
|
task.FailReason = taskResult.Reason
|
||||||
|
common.LogInfo(ctx, fmt.Sprintf("Task %s failed: %s", task.TaskID, task.FailReason))
|
||||||
|
quota := task.Quota
|
||||||
|
if quota != 0 {
|
||||||
|
if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
|
||||||
|
common.LogError(ctx, "Failed to increase user quota: "+err.Error())
|
||||||
|
}
|
||||||
|
logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, common.LogQuota(quota))
|
||||||
|
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown task status %s for task %s", taskResult.Status, taskId)
|
||||||
|
}
|
||||||
|
if taskResult.Progress != "" {
|
||||||
|
task.Progress = taskResult.Progress
|
||||||
|
}
|
||||||
|
|
||||||
|
task.Data = responseBody
|
||||||
|
if err := task.Update(); err != nil {
|
||||||
|
common.SysError("UpdateVideoTask task error: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -12,15 +12,15 @@ func GetAllTokens(c *gin.Context) {
|
|||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
p, _ := strconv.Atoi(c.Query("p"))
|
p, _ := strconv.Atoi(c.Query("p"))
|
||||||
size, _ := strconv.Atoi(c.Query("size"))
|
size, _ := strconv.Atoi(c.Query("size"))
|
||||||
if p < 0 {
|
if p < 1 {
|
||||||
p = 0
|
p = 1
|
||||||
}
|
}
|
||||||
if size <= 0 {
|
if size <= 0 {
|
||||||
size = common.ItemsPerPage
|
size = common.ItemsPerPage
|
||||||
} else if size > 100 {
|
} else if size > 100 {
|
||||||
size = 100
|
size = 100
|
||||||
}
|
}
|
||||||
tokens, err := model.GetAllUserTokens(userId, p*size, size)
|
tokens, err := model.GetAllUserTokens(userId, (p-1)*size, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -28,10 +28,18 @@ func GetAllTokens(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Get total count for pagination
|
||||||
|
total, _ := model.CountUserTokens(userId)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": tokens,
|
"data": gin.H{
|
||||||
|
"items": tokens,
|
||||||
|
"total": total,
|
||||||
|
"page": p,
|
||||||
|
"page_size": size,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -250,3 +258,32 @@ func UpdateToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TokenBatch struct {
|
||||||
|
Ids []int `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTokenBatch(c *gin.Context) {
|
||||||
|
tokenBatch := TokenBatch{}
|
||||||
|
if err := c.ShouldBindJSON(&tokenBatch); err != nil || len(tokenBatch.Ids) == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
count, err := model.BatchDeleteTokens(tokenBatch.Ids, userId)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/Calcium-Ion/go-epay/epay"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
@@ -14,16 +11,21 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Calcium-Ion/go-epay/epay"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EpayRequest struct {
|
type EpayRequest struct {
|
||||||
Amount int `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
PaymentMethod string `json:"payment_method"`
|
PaymentMethod string `json:"payment_method"`
|
||||||
TopUpCode string `json:"top_up_code"`
|
TopUpCode string `json:"top_up_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AmountRequest struct {
|
type AmountRequest struct {
|
||||||
Amount int `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
TopUpCode string `json:"top_up_code"`
|
TopUpCode string `json:"top_up_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,25 +43,35 @@ func GetEpayClient() *epay.Client {
|
|||||||
return withUrl
|
return withUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPayMoney(amount float64, group string) float64 {
|
func getPayMoney(amount int64, group string) float64 {
|
||||||
|
dAmount := decimal.NewFromInt(amount)
|
||||||
|
|
||||||
if !common.DisplayInCurrencyEnabled {
|
if !common.DisplayInCurrencyEnabled {
|
||||||
amount = amount / common.QuotaPerUnit
|
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||||
|
dAmount = dAmount.Div(dQuotaPerUnit)
|
||||||
}
|
}
|
||||||
// 别问为什么用float64,问就是这么点钱没必要
|
|
||||||
topupGroupRatio := common.GetTopupGroupRatio(group)
|
topupGroupRatio := common.GetTopupGroupRatio(group)
|
||||||
if topupGroupRatio == 0 {
|
if topupGroupRatio == 0 {
|
||||||
topupGroupRatio = 1
|
topupGroupRatio = 1
|
||||||
}
|
}
|
||||||
payMoney := amount * setting.Price * topupGroupRatio
|
|
||||||
return payMoney
|
dTopupGroupRatio := decimal.NewFromFloat(topupGroupRatio)
|
||||||
|
dPrice := decimal.NewFromFloat(setting.Price)
|
||||||
|
|
||||||
|
payMoney := dAmount.Mul(dPrice).Mul(dTopupGroupRatio)
|
||||||
|
|
||||||
|
return payMoney.InexactFloat64()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMinTopup() int {
|
func getMinTopup() int64 {
|
||||||
minTopup := setting.MinTopUp
|
minTopup := setting.MinTopUp
|
||||||
if !common.DisplayInCurrencyEnabled {
|
if !common.DisplayInCurrencyEnabled {
|
||||||
minTopup = minTopup * int(common.QuotaPerUnit)
|
dMinTopup := decimal.NewFromInt(int64(minTopup))
|
||||||
|
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||||
|
minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
|
||||||
}
|
}
|
||||||
return minTopup
|
return int64(minTopup)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RequestEpay(c *gin.Context) {
|
func RequestEpay(c *gin.Context) {
|
||||||
@@ -80,21 +92,19 @@ func RequestEpay(c *gin.Context) {
|
|||||||
c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
|
c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payMoney := getPayMoney(float64(req.Amount), group)
|
payMoney := getPayMoney(req.Amount, group)
|
||||||
if payMoney < 0.01 {
|
if payMoney < 0.01 {
|
||||||
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
|
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payType := "wxpay"
|
|
||||||
if req.PaymentMethod == "zfb" {
|
if !setting.ContainsPayMethod(req.PaymentMethod) {
|
||||||
payType = "alipay"
|
c.JSON(200, gin.H{"message": "error", "data": "支付方式不存在"})
|
||||||
}
|
return
|
||||||
if req.PaymentMethod == "wx" {
|
|
||||||
req.PaymentMethod = "wxpay"
|
|
||||||
payType = "wxpay"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callBackAddress := service.GetCallbackAddress()
|
callBackAddress := service.GetCallbackAddress()
|
||||||
returnUrl, _ := url.Parse(setting.ServerAddress + "/log")
|
returnUrl, _ := url.Parse(setting.ServerAddress + "/console/log")
|
||||||
notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
|
notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
|
||||||
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
|
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
|
||||||
tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
|
tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
|
||||||
@@ -104,7 +114,7 @@ func RequestEpay(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
uri, params, err := client.Purchase(&epay.PurchaseArgs{
|
uri, params, err := client.Purchase(&epay.PurchaseArgs{
|
||||||
Type: payType,
|
Type: req.PaymentMethod,
|
||||||
ServiceTradeNo: tradeNo,
|
ServiceTradeNo: tradeNo,
|
||||||
Name: fmt.Sprintf("TUC%d", req.Amount),
|
Name: fmt.Sprintf("TUC%d", req.Amount),
|
||||||
Money: strconv.FormatFloat(payMoney, 'f', 2, 64),
|
Money: strconv.FormatFloat(payMoney, 'f', 2, 64),
|
||||||
@@ -118,7 +128,9 @@ func RequestEpay(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
amount := req.Amount
|
amount := req.Amount
|
||||||
if !common.DisplayInCurrencyEnabled {
|
if !common.DisplayInCurrencyEnabled {
|
||||||
amount = amount / int(common.QuotaPerUnit)
|
dAmount := decimal.NewFromInt(int64(amount))
|
||||||
|
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||||
|
amount = dAmount.Div(dQuotaPerUnit).IntPart()
|
||||||
}
|
}
|
||||||
topUp := &model.TopUp{
|
topUp := &model.TopUp{
|
||||||
UserId: id,
|
UserId: id,
|
||||||
@@ -210,13 +222,16 @@ func EpayNotify(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
//user, _ := model.GetUserById(topUp.UserId, false)
|
//user, _ := model.GetUserById(topUp.UserId, false)
|
||||||
//user.Quota += topUp.Amount * 500000
|
//user.Quota += topUp.Amount * 500000
|
||||||
err = model.IncreaseUserQuota(topUp.UserId, topUp.Amount*int(common.QuotaPerUnit))
|
dAmount := decimal.NewFromInt(int64(topUp.Amount))
|
||||||
|
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||||
|
quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart())
|
||||||
|
err = model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("易支付回调更新用户失败: %v", topUp)
|
log.Printf("易支付回调更新用户失败: %v", topUp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("易支付回调更新用户成功 %v", topUp)
|
log.Printf("易支付回调更新用户成功 %v", topUp)
|
||||||
model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", common.LogQuota(topUp.Amount*int(common.QuotaPerUnit)), topUp.Money))
|
model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", common.LogQuota(quotaToAdd), topUp.Money))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("易支付异常回调: %v", verifyInfo)
|
log.Printf("易支付异常回调: %v", verifyInfo)
|
||||||
@@ -241,7 +256,7 @@ func RequestAmount(c *gin.Context) {
|
|||||||
c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
|
c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payMoney := getPayMoney(float64(req.Amount), group)
|
payMoney := getPayMoney(req.Amount, group)
|
||||||
if payMoney <= 0.01 {
|
if payMoney <= 0.01 {
|
||||||
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
|
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
|
||||||
return
|
return
|
||||||
|
|||||||
154
controller/uptime_kuma.go
Normal file
154
controller/uptime_kuma.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"one-api/setting/console_setting"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
requestTimeout = 30 * time.Second
|
||||||
|
httpTimeout = 10 * time.Second
|
||||||
|
uptimeKeySuffix = "_24"
|
||||||
|
apiStatusPath = "/api/status-page/"
|
||||||
|
apiHeartbeatPath = "/api/status-page/heartbeat/"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Monitor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Uptime float64 `json:"uptime"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
Group string `json:"group,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UptimeGroupResult struct {
|
||||||
|
CategoryName string `json:"categoryName"`
|
||||||
|
Monitors []Monitor `json:"monitors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAndDecode(ctx context.Context, client *http.Client, url string, dest interface{}) error {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errors.New("non-200 status")
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.NewDecoder(resp.Body).Decode(dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchGroupData(ctx context.Context, client *http.Client, groupConfig map[string]interface{}) UptimeGroupResult {
|
||||||
|
url, _ := groupConfig["url"].(string)
|
||||||
|
slug, _ := groupConfig["slug"].(string)
|
||||||
|
categoryName, _ := groupConfig["categoryName"].(string)
|
||||||
|
|
||||||
|
result := UptimeGroupResult{
|
||||||
|
CategoryName: categoryName,
|
||||||
|
Monitors: []Monitor{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if url == "" || slug == "" {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := strings.TrimSuffix(url, "/")
|
||||||
|
|
||||||
|
var statusData struct {
|
||||||
|
PublicGroupList []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
MonitorList []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"monitorList"`
|
||||||
|
} `json:"publicGroupList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var heartbeatData struct {
|
||||||
|
HeartbeatList map[string][]struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"heartbeatList"`
|
||||||
|
UptimeList map[string]float64 `json:"uptimeList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
g, gCtx := errgroup.WithContext(ctx)
|
||||||
|
g.Go(func() error {
|
||||||
|
return getAndDecode(gCtx, client, baseURL+apiStatusPath+slug, &statusData)
|
||||||
|
})
|
||||||
|
g.Go(func() error {
|
||||||
|
return getAndDecode(gCtx, client, baseURL+apiHeartbeatPath+slug, &heartbeatData)
|
||||||
|
})
|
||||||
|
|
||||||
|
if g.Wait() != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pg := range statusData.PublicGroupList {
|
||||||
|
if len(pg.MonitorList) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range pg.MonitorList {
|
||||||
|
monitor := Monitor{
|
||||||
|
Name: m.Name,
|
||||||
|
Group: pg.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
monitorID := strconv.Itoa(m.ID)
|
||||||
|
|
||||||
|
if uptime, exists := heartbeatData.UptimeList[monitorID+uptimeKeySuffix]; exists {
|
||||||
|
monitor.Uptime = uptime
|
||||||
|
}
|
||||||
|
|
||||||
|
if heartbeats, exists := heartbeatData.HeartbeatList[monitorID]; exists && len(heartbeats) > 0 {
|
||||||
|
monitor.Status = heartbeats[0].Status
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Monitors = append(result.Monitors, monitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUptimeKumaStatus(c *gin.Context) {
|
||||||
|
groups := console_setting.GetUptimeKumaGroups()
|
||||||
|
if len(groups) == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": []UptimeGroupResult{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: httpTimeout}
|
||||||
|
results := make([]UptimeGroupResult, len(groups))
|
||||||
|
|
||||||
|
g, gCtx := errgroup.WithContext(ctx)
|
||||||
|
for i, group := range groups {
|
||||||
|
i, group := i, group
|
||||||
|
g.Go(func() error {
|
||||||
|
results[i] = fetchGroupData(gCtx, client, group)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Wait()
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": results})
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/dto"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -226,6 +227,9 @@ func Register(c *gin.Context) {
|
|||||||
UnlimitedQuota: true,
|
UnlimitedQuota: true,
|
||||||
ModelLimitsEnabled: false,
|
ModelLimitsEnabled: false,
|
||||||
}
|
}
|
||||||
|
if setting.DefaultUseAutoGroup {
|
||||||
|
token.Group = "auto"
|
||||||
|
}
|
||||||
if err := token.Insert(); err != nil {
|
if err := token.Insert(); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -243,15 +247,15 @@ func Register(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetAllUsers(c *gin.Context) {
|
func GetAllUsers(c *gin.Context) {
|
||||||
p, _ := strconv.Atoi(c.Query("p"))
|
pageInfo, err := common.GetPageQuery(c)
|
||||||
pageSize, _ := strconv.Atoi(c.Query("page_size"))
|
if err != nil {
|
||||||
if p < 1 {
|
c.JSON(http.StatusOK, gin.H{
|
||||||
p = 1
|
"success": false,
|
||||||
|
"message": "parse page query failed",
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if pageSize < 0 {
|
users, total, err := model.GetAllUsers(pageInfo)
|
||||||
pageSize = common.ItemsPerPage
|
|
||||||
}
|
|
||||||
users, total, err := model.GetAllUsers((p-1)*pageSize, pageSize)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -259,15 +263,13 @@ func GetAllUsers(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageInfo.SetTotal(int(total))
|
||||||
|
pageInfo.SetItems(users)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": gin.H{
|
"data": pageInfo,
|
||||||
"items": users,
|
|
||||||
"total": total,
|
|
||||||
"page": p,
|
|
||||||
"page_size": pageSize,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -459,6 +461,9 @@ func GetSelf(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Hide admin remarks: set to empty to trigger omitempty tag, ensuring the remark field is not included in JSON returned to regular users
|
||||||
|
user.Remark = ""
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
@@ -483,7 +488,7 @@ func GetUserModels(c *gin.Context) {
|
|||||||
groups := setting.GetUserUsableGroups(user.Group)
|
groups := setting.GetUserUsableGroups(user.Group)
|
||||||
var models []string
|
var models []string
|
||||||
for group := range groups {
|
for group := range groups {
|
||||||
for _, g := range model.GetGroupModels(group) {
|
for _, g := range model.GetGroupEnabledModels(group) {
|
||||||
if !common.StringsContains(models, g) {
|
if !common.StringsContains(models, g) {
|
||||||
models = append(models, g)
|
models = append(models, g)
|
||||||
}
|
}
|
||||||
@@ -592,7 +597,14 @@ func UpdateSelf(c *gin.Context) {
|
|||||||
user.Password = "" // rollback to what it should be
|
user.Password = "" // rollback to what it should be
|
||||||
cleanUser.Password = ""
|
cleanUser.Password = ""
|
||||||
}
|
}
|
||||||
updatePassword := user.Password != ""
|
updatePassword, err := checkUpdatePassword(user.OriginalPassword, user.Password, cleanUser.Id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := cleanUser.Update(updatePassword); err != nil {
|
if err := cleanUser.Update(updatePassword); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -608,6 +620,23 @@ func UpdateSelf(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkUpdatePassword(originalPassword string, newPassword string, userId int) (updatePassword bool, err error) {
|
||||||
|
var currentUser *model.User
|
||||||
|
currentUser, err = model.GetUserById(userId, true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !common.ValidatePasswordAndHash(originalPassword, currentUser.Password) {
|
||||||
|
err = fmt.Errorf("原密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if newPassword == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatePassword = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteUser(c *gin.Context) {
|
func DeleteUser(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -913,11 +942,13 @@ func TopUp(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserSettingRequest struct {
|
type UpdateUserSettingRequest struct {
|
||||||
QuotaWarningType string `json:"notify_type"`
|
QuotaWarningType string `json:"notify_type"`
|
||||||
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
|
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
|
||||||
WebhookUrl string `json:"webhook_url,omitempty"`
|
WebhookUrl string `json:"webhook_url,omitempty"`
|
||||||
WebhookSecret string `json:"webhook_secret,omitempty"`
|
WebhookSecret string `json:"webhook_secret,omitempty"`
|
||||||
NotificationEmail string `json:"notification_email,omitempty"`
|
NotificationEmail string `json:"notification_email,omitempty"`
|
||||||
|
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
|
||||||
|
RecordIpLog bool `json:"record_ip_log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateUserSetting(c *gin.Context) {
|
func UpdateUserSetting(c *gin.Context) {
|
||||||
@@ -931,7 +962,7 @@ func UpdateUserSetting(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证预警类型
|
// 验证预警类型
|
||||||
if req.QuotaWarningType != constant.NotifyTypeEmail && req.QuotaWarningType != constant.NotifyTypeWebhook {
|
if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "无效的预警类型",
|
"message": "无效的预警类型",
|
||||||
@@ -949,7 +980,7 @@ func UpdateUserSetting(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果是webhook类型,验证webhook地址
|
// 如果是webhook类型,验证webhook地址
|
||||||
if req.QuotaWarningType == constant.NotifyTypeWebhook {
|
if req.QuotaWarningType == dto.NotifyTypeWebhook {
|
||||||
if req.WebhookUrl == "" {
|
if req.WebhookUrl == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -968,7 +999,7 @@ func UpdateUserSetting(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果是邮件类型,验证邮箱地址
|
// 如果是邮件类型,验证邮箱地址
|
||||||
if req.QuotaWarningType == constant.NotifyTypeEmail && req.NotificationEmail != "" {
|
if req.QuotaWarningType == dto.NotifyTypeEmail && req.NotificationEmail != "" {
|
||||||
// 验证邮箱格式
|
// 验证邮箱格式
|
||||||
if !strings.Contains(req.NotificationEmail, "@") {
|
if !strings.Contains(req.NotificationEmail, "@") {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -990,22 +1021,24 @@ func UpdateUserSetting(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建设置
|
// 构建设置
|
||||||
settings := map[string]interface{}{
|
settings := dto.UserSetting{
|
||||||
constant.UserSettingNotifyType: req.QuotaWarningType,
|
NotifyType: req.QuotaWarningType,
|
||||||
constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold,
|
QuotaWarningThreshold: req.QuotaWarningThreshold,
|
||||||
|
AcceptUnsetRatioModel: req.AcceptUnsetModelRatioModel,
|
||||||
|
RecordIpLog: req.RecordIpLog,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是webhook类型,添加webhook相关设置
|
// 如果是webhook类型,添加webhook相关设置
|
||||||
if req.QuotaWarningType == constant.NotifyTypeWebhook {
|
if req.QuotaWarningType == dto.NotifyTypeWebhook {
|
||||||
settings[constant.UserSettingWebhookUrl] = req.WebhookUrl
|
settings.WebhookUrl = req.WebhookUrl
|
||||||
if req.WebhookSecret != "" {
|
if req.WebhookSecret != "" {
|
||||||
settings[constant.UserSettingWebhookSecret] = req.WebhookSecret
|
settings.WebhookSecret = req.WebhookSecret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果提供了通知邮箱,添加到设置中
|
// 如果提供了通知邮箱,添加到设置中
|
||||||
if req.QuotaWarningType == constant.NotifyTypeEmail && req.NotificationEmail != "" {
|
if req.QuotaWarningType == dto.NotifyTypeEmail && req.NotificationEmail != "" {
|
||||||
settings[constant.UserSettingNotificationEmail] = req.NotificationEmail
|
settings.NotificationEmail = req.NotificationEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户设置
|
// 更新用户设置
|
||||||
|
|||||||
@@ -11,15 +11,17 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
|
- ${JS_SCRIPT_DIR:-./scripts}:/app/scripts
|
||||||
environment:
|
environment:
|
||||||
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service
|
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service
|
||||||
- REDIS_CONN_STRING=redis://redis
|
- REDIS_CONN_STRING=redis://redis
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录
|
||||||
|
# - STREAMING_TIMEOUT=120 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值
|
||||||
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
|
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
|
||||||
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
|
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
|
||||||
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
|
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
|
||||||
# - FRONTEND_BASE_URL=https://openai.justsong.cn # Uncomment for multi-node deployment with front-end URL
|
# - FRONTEND_BASE_URL=https://openai.justsong.cn # Uncomment for multi-node deployment with front-end URL
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- mysql
|
- mysql
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
- 类型为字符串,填写代理地址(例如 socks5 协议的代理地址)
|
- 类型为字符串,填写代理地址(例如 socks5 协议的代理地址)
|
||||||
|
|
||||||
3. thinking_to_content
|
3. thinking_to_content
|
||||||
- 用于标识是否将思考内容`reasoning_conetnt`转换为`<think>`标签拼接到内容中返回
|
- 用于标识是否将思考内容`reasoning_content`转换为`<think>`标签拼接到内容中返回
|
||||||
- 类型为布尔值,设置为 true 时启用思考内容转换
|
- 类型为布尔值,设置为 true 时启用思考内容转换
|
||||||
|
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
@@ -30,4 +30,4 @@
|
|||||||
|
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
|
|
||||||
通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
|
通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
密钥为环境变量SESSION_SECRET
|
密钥为环境变量SESSION_SECRET
|
||||||
|
|
||||||

|

|
||||||
7
dto/channel_settings.go
Normal file
7
dto/channel_settings.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type ChannelSettings struct {
|
||||||
|
ForceFormat bool `json:"force_format,omitempty"`
|
||||||
|
ThinkingToContent bool `json:"thinking_to_content,omitempty"`
|
||||||
|
Proxy string `json:"proxy"`
|
||||||
|
}
|
||||||
257
dto/claude.go
Normal file
257
dto/claude.go
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"one-api/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClaudeMetadata struct {
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeMediaMessage struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Text *string `json:"text,omitempty"`
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
|
Source *ClaudeMessageSource `json:"source,omitempty"`
|
||||||
|
Usage *ClaudeUsage `json:"usage,omitempty"`
|
||||||
|
StopReason *string `json:"stop_reason,omitempty"`
|
||||||
|
PartialJson *string `json:"partial_json,omitempty"`
|
||||||
|
Role string `json:"role,omitempty"`
|
||||||
|
Thinking string `json:"thinking,omitempty"`
|
||||||
|
Signature string `json:"signature,omitempty"`
|
||||||
|
Delta string `json:"delta,omitempty"`
|
||||||
|
CacheControl json.RawMessage `json:"cache_control,omitempty"`
|
||||||
|
// tool_calls
|
||||||
|
Id string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Input any `json:"input,omitempty"`
|
||||||
|
Content any `json:"content,omitempty"`
|
||||||
|
ToolUseId string `json:"tool_use_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMediaMessage) SetText(s string) {
|
||||||
|
c.Text = &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMediaMessage) GetText() string {
|
||||||
|
if c.Text == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *c.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMediaMessage) IsStringContent() bool {
|
||||||
|
if c.Content == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := c.Content.(string)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMediaMessage) GetStringContent() string {
|
||||||
|
if c.Content == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch c.Content.(type) {
|
||||||
|
case string:
|
||||||
|
return c.Content.(string)
|
||||||
|
case []any:
|
||||||
|
var contentStr string
|
||||||
|
for _, contentItem := range c.Content.([]any) {
|
||||||
|
contentMap, ok := contentItem.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if contentMap["type"] == ContentTypeText {
|
||||||
|
if subStr, ok := contentMap["text"].(string); ok {
|
||||||
|
contentStr += subStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentStr
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMediaMessage) GetJsonRowString() string {
|
||||||
|
jsonContent, _ := json.Marshal(c)
|
||||||
|
return string(jsonContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMediaMessage) SetContent(content any) {
|
||||||
|
c.Content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMediaMessage) ParseMediaContent() []ClaudeMediaMessage {
|
||||||
|
mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.Content)
|
||||||
|
return mediaContent
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeMessageSource struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
MediaType string `json:"media_type,omitempty"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
Url string `json:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content any `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMessage) IsStringContent() bool {
|
||||||
|
if c.Content == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := c.Content.(string)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMessage) GetStringContent() string {
|
||||||
|
if c.Content == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch c.Content.(type) {
|
||||||
|
case string:
|
||||||
|
return c.Content.(string)
|
||||||
|
case []any:
|
||||||
|
var contentStr string
|
||||||
|
for _, contentItem := range c.Content.([]any) {
|
||||||
|
contentMap, ok := contentItem.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if contentMap["type"] == ContentTypeText {
|
||||||
|
if subStr, ok := contentMap["text"].(string); ok {
|
||||||
|
contentStr += subStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentStr
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMessage) SetStringContent(content string) {
|
||||||
|
c.Content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) {
|
||||||
|
return common.Any2Type[[]ClaudeMediaMessage](c.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tool struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
InputSchema map[string]interface{} `json:"input_schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputSchema struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Properties any `json:"properties,omitempty"`
|
||||||
|
Required any `json:"required,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Prompt string `json:"prompt,omitempty"`
|
||||||
|
System any `json:"system,omitempty"`
|
||||||
|
Messages []ClaudeMessage `json:"messages,omitempty"`
|
||||||
|
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||||
|
MaxTokensToSample uint `json:"max_tokens_to_sample,omitempty"`
|
||||||
|
StopSequences []string `json:"stop_sequences,omitempty"`
|
||||||
|
Temperature *float64 `json:"temperature,omitempty"`
|
||||||
|
TopP float64 `json:"top_p,omitempty"`
|
||||||
|
TopK int `json:"top_k,omitempty"`
|
||||||
|
//ClaudeMetadata `json:"metadata,omitempty"`
|
||||||
|
Stream bool `json:"stream,omitempty"`
|
||||||
|
Tools any `json:"tools,omitempty"`
|
||||||
|
ToolChoice any `json:"tool_choice,omitempty"`
|
||||||
|
Thinking *Thinking `json:"thinking,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Thinking struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
BudgetTokens *int `json:"budget_tokens,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Thinking) GetBudgetTokens() int {
|
||||||
|
if c.BudgetTokens == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *c.BudgetTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeRequest) IsStringSystem() bool {
|
||||||
|
_, ok := c.System.(string)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeRequest) GetStringSystem() string {
|
||||||
|
if c.IsStringSystem() {
|
||||||
|
return c.System.(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeRequest) SetStringSystem(system string) {
|
||||||
|
c.System = system
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeRequest) ParseSystem() []ClaudeMediaMessage {
|
||||||
|
mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.System)
|
||||||
|
return mediaContent
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeError struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeErrorWithStatusCode struct {
|
||||||
|
Error ClaudeError `json:"error"`
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
LocalError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeResponse struct {
|
||||||
|
Id string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Role string `json:"role,omitempty"`
|
||||||
|
Content []ClaudeMediaMessage `json:"content,omitempty"`
|
||||||
|
Completion string `json:"completion,omitempty"`
|
||||||
|
StopReason string `json:"stop_reason,omitempty"`
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
|
Error *ClaudeError `json:"error,omitempty"`
|
||||||
|
Usage *ClaudeUsage `json:"usage,omitempty"`
|
||||||
|
Index *int `json:"index,omitempty"`
|
||||||
|
ContentBlock *ClaudeMediaMessage `json:"content_block,omitempty"`
|
||||||
|
Delta *ClaudeMediaMessage `json:"delta,omitempty"`
|
||||||
|
Message *ClaudeMediaMessage `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// set index
|
||||||
|
func (c *ClaudeResponse) SetIndex(i int) {
|
||||||
|
c.Index = &i
|
||||||
|
}
|
||||||
|
|
||||||
|
// get index
|
||||||
|
func (c *ClaudeResponse) GetIndex() int {
|
||||||
|
if c.Index == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *c.Index
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeUsage struct {
|
||||||
|
InputTokens int `json:"input_tokens"`
|
||||||
|
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
|
||||||
|
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
||||||
|
OutputTokens int `json:"output_tokens"`
|
||||||
|
}
|
||||||
23
dto/dalle.go
23
dto/dalle.go
@@ -1,14 +1,21 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
type ImageRequest struct {
|
type ImageRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Prompt string `json:"prompt" binding:"required"`
|
Prompt string `json:"prompt" binding:"required"`
|
||||||
N int `json:"n,omitempty"`
|
N int `json:"n,omitempty"`
|
||||||
Size string `json:"size,omitempty"`
|
Size string `json:"size,omitempty"`
|
||||||
Quality string `json:"quality,omitempty"`
|
Quality string `json:"quality,omitempty"`
|
||||||
ResponseFormat string `json:"response_format,omitempty"`
|
ResponseFormat string `json:"response_format,omitempty"`
|
||||||
Style string `json:"style,omitempty"`
|
Style string `json:"style,omitempty"`
|
||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
|
ExtraFields json.RawMessage `json:"extra_fields,omitempty"`
|
||||||
|
Background string `json:"background,omitempty"`
|
||||||
|
Moderation string `json:"moderation,omitempty"`
|
||||||
|
OutputFormat string `json:"output_format,omitempty"`
|
||||||
|
Watermark *bool `json:"watermark,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageResponse struct {
|
type ImageResponse struct {
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ type MidjourneyDto struct {
|
|||||||
StartTime int64 `json:"startTime"`
|
StartTime int64 `json:"startTime"`
|
||||||
FinishTime int64 `json:"finishTime"`
|
FinishTime int64 `json:"finishTime"`
|
||||||
ImageUrl string `json:"imageUrl"`
|
ImageUrl string `json:"imageUrl"`
|
||||||
|
VideoUrl string `json:"videoUrl"`
|
||||||
|
VideoUrls []ImgUrls `json:"videoUrls"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Progress string `json:"progress"`
|
Progress string `json:"progress"`
|
||||||
FailReason string `json:"failReason"`
|
FailReason string `json:"failReason"`
|
||||||
@@ -65,6 +67,10 @@ type MidjourneyDto struct {
|
|||||||
Properties *Properties `json:"properties"`
|
Properties *Properties `json:"properties"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImgUrls struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
type MidjourneyStatus struct {
|
type MidjourneyStatus struct {
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"one-api/common"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type ResponseFormat struct {
|
type ResponseFormat struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
@@ -15,60 +19,79 @@ type FormatJsonSchema struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GeneralOpenAIRequest struct {
|
type GeneralOpenAIRequest struct {
|
||||||
Model string `json:"model,omitempty"`
|
Model string `json:"model,omitempty"`
|
||||||
Messages []Message `json:"messages,omitempty"`
|
Messages []Message `json:"messages,omitempty"`
|
||||||
Prompt any `json:"prompt,omitempty"`
|
Prompt any `json:"prompt,omitempty"`
|
||||||
Prefix any `json:"prefix,omitempty"`
|
Prefix any `json:"prefix,omitempty"`
|
||||||
Suffix any `json:"suffix,omitempty"`
|
Suffix any `json:"suffix,omitempty"`
|
||||||
Stream bool `json:"stream,omitempty"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||||
MaxTokens uint `json:"max_tokens,omitempty"`
|
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||||
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
|
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
|
||||||
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
||||||
Temperature *float64 `json:"temperature,omitempty"`
|
Temperature *float64 `json:"temperature,omitempty"`
|
||||||
TopP float64 `json:"top_p,omitempty"`
|
TopP float64 `json:"top_p,omitempty"`
|
||||||
TopK int `json:"top_k,omitempty"`
|
TopK int `json:"top_k,omitempty"`
|
||||||
Stop any `json:"stop,omitempty"`
|
Stop any `json:"stop,omitempty"`
|
||||||
N int `json:"n,omitempty"`
|
N int `json:"n,omitempty"`
|
||||||
Input any `json:"input,omitempty"`
|
Input any `json:"input,omitempty"`
|
||||||
Instruction string `json:"instruction,omitempty"`
|
Instruction string `json:"instruction,omitempty"`
|
||||||
Size string `json:"size,omitempty"`
|
Size string `json:"size,omitempty"`
|
||||||
Functions any `json:"functions,omitempty"`
|
Functions json.RawMessage `json:"functions,omitempty"`
|
||||||
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
|
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
|
||||||
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
||||||
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
|
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
|
||||||
EncodingFormat any `json:"encoding_format,omitempty"`
|
EncodingFormat json.RawMessage `json:"encoding_format,omitempty"`
|
||||||
Seed float64 `json:"seed,omitempty"`
|
Seed float64 `json:"seed,omitempty"`
|
||||||
Tools []ToolCall `json:"tools,omitempty"`
|
ParallelTooCalls *bool `json:"parallel_tool_calls,omitempty"`
|
||||||
ToolChoice any `json:"tool_choice,omitempty"`
|
Tools []ToolCallRequest `json:"tools,omitempty"`
|
||||||
User string `json:"user,omitempty"`
|
ToolChoice any `json:"tool_choice,omitempty"`
|
||||||
LogProbs bool `json:"logprobs,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
TopLogProbs int `json:"top_logprobs,omitempty"`
|
LogProbs bool `json:"logprobs,omitempty"`
|
||||||
Dimensions int `json:"dimensions,omitempty"`
|
TopLogProbs int `json:"top_logprobs,omitempty"`
|
||||||
Modalities any `json:"modalities,omitempty"`
|
Dimensions int `json:"dimensions,omitempty"`
|
||||||
Audio any `json:"audio,omitempty"`
|
Modalities json.RawMessage `json:"modalities,omitempty"`
|
||||||
|
Audio json.RawMessage `json:"audio,omitempty"`
|
||||||
|
EnableThinking any `json:"enable_thinking,omitempty"` // ali
|
||||||
|
THINKING json.RawMessage `json:"thinking,omitempty"` // doubao
|
||||||
|
ExtraBody json.RawMessage `json:"extra_body,omitempty"`
|
||||||
|
WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
|
||||||
|
// OpenRouter Params
|
||||||
|
Usage json.RawMessage `json:"usage,omitempty"`
|
||||||
|
Reasoning json.RawMessage `json:"reasoning,omitempty"`
|
||||||
|
// Ali Qwen Params
|
||||||
|
VlHighResolutionImages json.RawMessage `json:"vl_high_resolution_images,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAITools struct {
|
func (r *GeneralOpenAIRequest) ToMap() map[string]any {
|
||||||
Type string `json:"type"`
|
result := make(map[string]any)
|
||||||
Function OpenAIFunction `json:"function"`
|
data, _ := common.EncodeJson(r)
|
||||||
|
_ = common.UnmarshalJson(data, &result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIFunction struct {
|
type ToolCallRequest struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Function FunctionRequest `json:"function"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FunctionRequest struct {
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Parameters any `json:"parameters,omitempty"`
|
Parameters any `json:"parameters,omitempty"`
|
||||||
|
Arguments string `json:"arguments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamOptions struct {
|
type StreamOptions struct {
|
||||||
IncludeUsage bool `json:"include_usage,omitempty"`
|
IncludeUsage bool `json:"include_usage,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r GeneralOpenAIRequest) GetMaxTokens() int {
|
func (r *GeneralOpenAIRequest) GetMaxTokens() int {
|
||||||
return int(r.MaxTokens)
|
return int(r.MaxTokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r GeneralOpenAIRequest) ParseInput() []string {
|
func (r *GeneralOpenAIRequest) ParseInput() []string {
|
||||||
if r.Input == nil {
|
if r.Input == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -88,15 +111,16 @@ func (r GeneralOpenAIRequest) ParseInput() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content json.RawMessage `json:"content"`
|
Content any `json:"content"`
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
Prefix *bool `json:"prefix,omitempty"`
|
Prefix *bool `json:"prefix,omitempty"`
|
||||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
Reasoning string `json:"reasoning,omitempty"`
|
||||||
ToolCallId string `json:"tool_call_id,omitempty"`
|
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
||||||
parsedContent []MediaContent
|
ToolCallId string `json:"tool_call_id,omitempty"`
|
||||||
parsedStringContent *string
|
parsedContent []MediaContent
|
||||||
|
//parsedStringContent *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaContent struct {
|
type MediaContent struct {
|
||||||
@@ -104,11 +128,70 @@ type MediaContent struct {
|
|||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
ImageUrl any `json:"image_url,omitempty"`
|
ImageUrl any `json:"image_url,omitempty"`
|
||||||
InputAudio any `json:"input_audio,omitempty"`
|
InputAudio any `json:"input_audio,omitempty"`
|
||||||
|
File any `json:"file,omitempty"`
|
||||||
|
VideoUrl any `json:"video_url,omitempty"`
|
||||||
|
// OpenRouter Params
|
||||||
|
CacheControl json.RawMessage `json:"cache_control,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MediaContent) GetImageMedia() *MessageImageUrl {
|
||||||
|
if m.ImageUrl != nil {
|
||||||
|
if _, ok := m.ImageUrl.(*MessageImageUrl); ok {
|
||||||
|
return m.ImageUrl.(*MessageImageUrl)
|
||||||
|
}
|
||||||
|
if itemMap, ok := m.ImageUrl.(map[string]any); ok {
|
||||||
|
out := &MessageImageUrl{
|
||||||
|
Url: common.Interface2String(itemMap["url"]),
|
||||||
|
Detail: common.Interface2String(itemMap["detail"]),
|
||||||
|
MimeType: common.Interface2String(itemMap["mime_type"]),
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MediaContent) GetInputAudio() *MessageInputAudio {
|
||||||
|
if m.InputAudio != nil {
|
||||||
|
if _, ok := m.InputAudio.(*MessageInputAudio); ok {
|
||||||
|
return m.InputAudio.(*MessageInputAudio)
|
||||||
|
}
|
||||||
|
if itemMap, ok := m.InputAudio.(map[string]any); ok {
|
||||||
|
out := &MessageInputAudio{
|
||||||
|
Data: common.Interface2String(itemMap["data"]),
|
||||||
|
Format: common.Interface2String(itemMap["format"]),
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MediaContent) GetFile() *MessageFile {
|
||||||
|
if m.File != nil {
|
||||||
|
if _, ok := m.File.(*MessageFile); ok {
|
||||||
|
return m.File.(*MessageFile)
|
||||||
|
}
|
||||||
|
if itemMap, ok := m.File.(map[string]any); ok {
|
||||||
|
out := &MessageFile{
|
||||||
|
FileName: common.Interface2String(itemMap["file_name"]),
|
||||||
|
FileData: common.Interface2String(itemMap["file_data"]),
|
||||||
|
FileId: common.Interface2String(itemMap["file_id"]),
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageImageUrl struct {
|
type MessageImageUrl struct {
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Detail string `json:"detail"`
|
Detail string `json:"detail"`
|
||||||
|
MimeType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessageImageUrl) IsRemoteImage() bool {
|
||||||
|
return strings.HasPrefix(m.Url, "http")
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageInputAudio struct {
|
type MessageInputAudio struct {
|
||||||
@@ -116,10 +199,22 @@ type MessageInputAudio struct {
|
|||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MessageFile struct {
|
||||||
|
FileName string `json:"filename,omitempty"`
|
||||||
|
FileData string `json:"file_data,omitempty"`
|
||||||
|
FileId string `json:"file_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageVideoUrl struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ContentTypeText = "text"
|
ContentTypeText = "text"
|
||||||
ContentTypeImageURL = "image_url"
|
ContentTypeImageURL = "image_url"
|
||||||
ContentTypeInputAudio = "input_audio"
|
ContentTypeInputAudio = "input_audio"
|
||||||
|
ContentTypeFile = "file"
|
||||||
|
ContentTypeVideoUrl = "video_url" // 阿里百炼视频识别
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Message) GetPrefix() bool {
|
func (m *Message) GetPrefix() bool {
|
||||||
@@ -133,11 +228,11 @@ func (m *Message) SetPrefix(prefix bool) {
|
|||||||
m.Prefix = &prefix
|
m.Prefix = &prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) ParseToolCalls() []ToolCall {
|
func (m *Message) ParseToolCalls() []ToolCallRequest {
|
||||||
if m.ToolCalls == nil {
|
if m.ToolCalls == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var toolCalls []ToolCall
|
var toolCalls []ToolCallRequest
|
||||||
if err := json.Unmarshal(m.ToolCalls, &toolCalls); err == nil {
|
if err := json.Unmarshal(m.ToolCalls, &toolCalls); err == nil {
|
||||||
return toolCalls
|
return toolCalls
|
||||||
}
|
}
|
||||||
@@ -150,14 +245,213 @@ func (m *Message) SetToolCalls(toolCalls any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) StringContent() string {
|
func (m *Message) StringContent() string {
|
||||||
|
switch m.Content.(type) {
|
||||||
|
case string:
|
||||||
|
return m.Content.(string)
|
||||||
|
case []any:
|
||||||
|
var contentStr string
|
||||||
|
for _, contentItem := range m.Content.([]any) {
|
||||||
|
contentMap, ok := contentItem.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if contentMap["type"] == ContentTypeText {
|
||||||
|
if subStr, ok := contentMap["text"].(string); ok {
|
||||||
|
contentStr += subStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentStr
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) SetNullContent() {
|
||||||
|
m.Content = nil
|
||||||
|
m.parsedContent = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) SetStringContent(content string) {
|
||||||
|
m.Content = content
|
||||||
|
m.parsedContent = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) SetMediaContent(content []MediaContent) {
|
||||||
|
m.Content = content
|
||||||
|
m.parsedContent = content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) IsStringContent() bool {
|
||||||
|
_, ok := m.Content.(string)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) ParseContent() []MediaContent {
|
||||||
|
if m.Content == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(m.parsedContent) > 0 {
|
||||||
|
return m.parsedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentList []MediaContent
|
||||||
|
// 先尝试解析为字符串
|
||||||
|
content, ok := m.Content.(string)
|
||||||
|
if ok {
|
||||||
|
contentList = []MediaContent{{
|
||||||
|
Type: ContentTypeText,
|
||||||
|
Text: content,
|
||||||
|
}}
|
||||||
|
m.parsedContent = contentList
|
||||||
|
return contentList
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析为数组
|
||||||
|
//var arrayContent []map[string]interface{}
|
||||||
|
|
||||||
|
arrayContent, ok := m.Content.([]any)
|
||||||
|
if !ok {
|
||||||
|
return contentList
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, contentItemAny := range arrayContent {
|
||||||
|
mediaItem, ok := contentItemAny.(MediaContent)
|
||||||
|
if ok {
|
||||||
|
contentList = append(contentList, mediaItem)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem, ok := contentItemAny.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contentType, ok := contentItem["type"].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case ContentTypeText:
|
||||||
|
if text, ok := contentItem["text"].(string); ok {
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeText,
|
||||||
|
Text: text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case ContentTypeImageURL:
|
||||||
|
imageUrl := contentItem["image_url"]
|
||||||
|
temp := &MessageImageUrl{
|
||||||
|
Detail: "high",
|
||||||
|
}
|
||||||
|
switch v := imageUrl.(type) {
|
||||||
|
case string:
|
||||||
|
temp.Url = v
|
||||||
|
case map[string]interface{}:
|
||||||
|
url, ok1 := v["url"].(string)
|
||||||
|
detail, ok2 := v["detail"].(string)
|
||||||
|
if ok2 {
|
||||||
|
temp.Detail = detail
|
||||||
|
}
|
||||||
|
if ok1 {
|
||||||
|
temp.Url = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeImageURL,
|
||||||
|
ImageUrl: temp,
|
||||||
|
})
|
||||||
|
|
||||||
|
case ContentTypeInputAudio:
|
||||||
|
if audioData, ok := contentItem["input_audio"].(map[string]interface{}); ok {
|
||||||
|
data, ok1 := audioData["data"].(string)
|
||||||
|
format, ok2 := audioData["format"].(string)
|
||||||
|
if ok1 && ok2 {
|
||||||
|
temp := &MessageInputAudio{
|
||||||
|
Data: data,
|
||||||
|
Format: format,
|
||||||
|
}
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeInputAudio,
|
||||||
|
InputAudio: temp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ContentTypeFile:
|
||||||
|
if fileData, ok := contentItem["file"].(map[string]interface{}); ok {
|
||||||
|
fileId, ok3 := fileData["file_id"].(string)
|
||||||
|
if ok3 {
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeFile,
|
||||||
|
File: &MessageFile{
|
||||||
|
FileId: fileId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fileName, ok1 := fileData["filename"].(string)
|
||||||
|
fileDataStr, ok2 := fileData["file_data"].(string)
|
||||||
|
if ok1 && ok2 {
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeFile,
|
||||||
|
File: &MessageFile{
|
||||||
|
FileName: fileName,
|
||||||
|
FileData: fileDataStr,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ContentTypeVideoUrl:
|
||||||
|
if videoUrl, ok := contentItem["video_url"].(string); ok {
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeVideoUrl,
|
||||||
|
VideoUrl: &MessageVideoUrl{
|
||||||
|
Url: videoUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(contentList) > 0 {
|
||||||
|
m.parsedContent = contentList
|
||||||
|
}
|
||||||
|
return contentList
|
||||||
|
}
|
||||||
|
|
||||||
|
// old code
|
||||||
|
/*func (m *Message) StringContent() string {
|
||||||
if m.parsedStringContent != nil {
|
if m.parsedStringContent != nil {
|
||||||
return *m.parsedStringContent
|
return *m.parsedStringContent
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringContent string
|
var stringContent string
|
||||||
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
|
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
|
||||||
|
m.parsedStringContent = &stringContent
|
||||||
return stringContent
|
return stringContent
|
||||||
}
|
}
|
||||||
return string(m.Content)
|
|
||||||
|
contentStr := new(strings.Builder)
|
||||||
|
arrayContent := m.ParseContent()
|
||||||
|
for _, content := range arrayContent {
|
||||||
|
if content.Type == ContentTypeText {
|
||||||
|
contentStr.WriteString(content.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stringContent = contentStr.String()
|
||||||
|
m.parsedStringContent = &stringContent
|
||||||
|
|
||||||
|
return stringContent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) SetNullContent() {
|
||||||
|
m.Content = nil
|
||||||
|
m.parsedStringContent = nil
|
||||||
|
m.parsedContent = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) SetStringContent(content string) {
|
func (m *Message) SetStringContent(content string) {
|
||||||
@@ -224,46 +518,75 @@ func (m *Message) ParseContent() []MediaContent {
|
|||||||
|
|
||||||
case ContentTypeImageURL:
|
case ContentTypeImageURL:
|
||||||
imageUrl := contentItem["image_url"]
|
imageUrl := contentItem["image_url"]
|
||||||
|
temp := &MessageImageUrl{
|
||||||
|
Detail: "high",
|
||||||
|
}
|
||||||
switch v := imageUrl.(type) {
|
switch v := imageUrl.(type) {
|
||||||
case string:
|
case string:
|
||||||
contentList = append(contentList, MediaContent{
|
temp.Url = v
|
||||||
Type: ContentTypeImageURL,
|
|
||||||
ImageUrl: MessageImageUrl{
|
|
||||||
Url: v,
|
|
||||||
Detail: "high",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
url, ok1 := v["url"].(string)
|
url, ok1 := v["url"].(string)
|
||||||
detail, ok2 := v["detail"].(string)
|
detail, ok2 := v["detail"].(string)
|
||||||
if !ok2 {
|
if ok2 {
|
||||||
detail = "high"
|
temp.Detail = detail
|
||||||
}
|
}
|
||||||
if ok1 {
|
if ok1 {
|
||||||
contentList = append(contentList, MediaContent{
|
temp.Url = url
|
||||||
Type: ContentTypeImageURL,
|
|
||||||
ImageUrl: MessageImageUrl{
|
|
||||||
Url: url,
|
|
||||||
Detail: detail,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeImageURL,
|
||||||
|
ImageUrl: temp,
|
||||||
|
})
|
||||||
|
|
||||||
case ContentTypeInputAudio:
|
case ContentTypeInputAudio:
|
||||||
if audioData, ok := contentItem["input_audio"].(map[string]interface{}); ok {
|
if audioData, ok := contentItem["input_audio"].(map[string]interface{}); ok {
|
||||||
data, ok1 := audioData["data"].(string)
|
data, ok1 := audioData["data"].(string)
|
||||||
format, ok2 := audioData["format"].(string)
|
format, ok2 := audioData["format"].(string)
|
||||||
if ok1 && ok2 {
|
if ok1 && ok2 {
|
||||||
|
temp := &MessageInputAudio{
|
||||||
|
Data: data,
|
||||||
|
Format: format,
|
||||||
|
}
|
||||||
contentList = append(contentList, MediaContent{
|
contentList = append(contentList, MediaContent{
|
||||||
Type: ContentTypeInputAudio,
|
Type: ContentTypeInputAudio,
|
||||||
InputAudio: MessageInputAudio{
|
InputAudio: temp,
|
||||||
Data: data,
|
|
||||||
Format: format,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case ContentTypeFile:
|
||||||
|
if fileData, ok := contentItem["file"].(map[string]interface{}); ok {
|
||||||
|
fileId, ok3 := fileData["file_id"].(string)
|
||||||
|
if ok3 {
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeFile,
|
||||||
|
File: &MessageFile{
|
||||||
|
FileId: fileId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fileName, ok1 := fileData["filename"].(string)
|
||||||
|
fileDataStr, ok2 := fileData["file_data"].(string)
|
||||||
|
if ok1 && ok2 {
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeFile,
|
||||||
|
File: &MessageFile{
|
||||||
|
FileName: fileName,
|
||||||
|
FileData: fileDataStr,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ContentTypeVideoUrl:
|
||||||
|
if videoUrl, ok := contentItem["video_url"].(string); ok {
|
||||||
|
contentList = append(contentList, MediaContent{
|
||||||
|
Type: ContentTypeVideoUrl,
|
||||||
|
VideoUrl: &MessageVideoUrl{
|
||||||
|
Url: videoUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,4 +595,57 @@ func (m *Message) ParseContent() []MediaContent {
|
|||||||
m.parsedContent = contentList
|
m.parsedContent = contentList
|
||||||
}
|
}
|
||||||
return contentList
|
return contentList
|
||||||
|
}*/
|
||||||
|
|
||||||
|
type WebSearchOptions struct {
|
||||||
|
SearchContextSize string `json:"search_context_size,omitempty"`
|
||||||
|
UserLocation json.RawMessage `json:"user_location,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIResponsesRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Input json.RawMessage `json:"input,omitempty"`
|
||||||
|
Include json.RawMessage `json:"include,omitempty"`
|
||||||
|
Instructions json.RawMessage `json:"instructions,omitempty"`
|
||||||
|
MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
|
||||||
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||||
|
ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
|
||||||
|
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
||||||
|
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
||||||
|
ServiceTier string `json:"service_tier,omitempty"`
|
||||||
|
Store bool `json:"store,omitempty"`
|
||||||
|
Stream bool `json:"stream,omitempty"`
|
||||||
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
|
Text json.RawMessage `json:"text,omitempty"`
|
||||||
|
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||||
|
Tools []ResponsesToolsCall `json:"tools,omitempty"`
|
||||||
|
TopP float64 `json:"top_p,omitempty"`
|
||||||
|
Truncation string `json:"truncation,omitempty"`
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reasoning struct {
|
||||||
|
Effort string `json:"effort,omitempty"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponsesToolsCall struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
// Web Search
|
||||||
|
UserLocation json.RawMessage `json:"user_location,omitempty"`
|
||||||
|
SearchContextSize string `json:"search_context_size,omitempty"`
|
||||||
|
// File Search
|
||||||
|
VectorStoreIds []string `json:"vector_store_ids,omitempty"`
|
||||||
|
MaxNumResults uint `json:"max_num_results,omitempty"`
|
||||||
|
Filters json.RawMessage `json:"filters,omitempty"`
|
||||||
|
// Computer Use
|
||||||
|
DisplayWidth uint `json:"display_width,omitempty"`
|
||||||
|
DisplayHeight uint `json:"display_height,omitempty"`
|
||||||
|
Environment string `json:"environment,omitempty"`
|
||||||
|
// Function
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Parameters json.RawMessage `json:"parameters,omitempty"`
|
||||||
|
Function json.RawMessage `json:"function,omitempty"`
|
||||||
|
Container json.RawMessage `json:"container,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
type TextResponseWithError struct {
|
import "encoding/json"
|
||||||
Id string `json:"id"`
|
|
||||||
Object string `json:"object"`
|
|
||||||
Created int64 `json:"created"`
|
|
||||||
Choices []OpenAITextResponseChoice `json:"choices"`
|
|
||||||
Data []OpenAIEmbeddingResponseItem `json:"data"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Usage `json:"usage"`
|
|
||||||
Error OpenAIError `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SimpleResponse struct {
|
type SimpleResponse struct {
|
||||||
Usage `json:"usage"`
|
Usage `json:"usage"`
|
||||||
Error OpenAIError `json:"error"`
|
Error *OpenAIError `json:"error"`
|
||||||
Choices []OpenAITextResponseChoice `json:"choices"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextResponse struct {
|
type TextResponse struct {
|
||||||
@@ -36,8 +26,9 @@ type OpenAITextResponse struct {
|
|||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Object string `json:"object"`
|
Object string `json:"object"`
|
||||||
Created int64 `json:"created"`
|
Created any `json:"created"`
|
||||||
Choices []OpenAITextResponseChoice `json:"choices"`
|
Choices []OpenAITextResponseChoice `json:"choices"`
|
||||||
|
Error *OpenAIError `json:"error,omitempty"`
|
||||||
Usage `json:"usage"`
|
Usage `json:"usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +53,11 @@ type ChatCompletionsStreamResponseChoice struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChatCompletionsStreamResponseChoiceDelta struct {
|
type ChatCompletionsStreamResponseChoiceDelta struct {
|
||||||
Content *string `json:"content,omitempty"`
|
Content *string `json:"content,omitempty"`
|
||||||
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
||||||
Role string `json:"role,omitempty"`
|
Reasoning *string `json:"reasoning,omitempty"`
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
Role string `json:"role,omitempty"`
|
||||||
|
ToolCalls []ToolCallResponse `json:"tool_calls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ChatCompletionsStreamResponseChoiceDelta) SetContentString(s string) {
|
func (c *ChatCompletionsStreamResponseChoiceDelta) SetContentString(s string) {
|
||||||
@@ -80,34 +72,38 @@ func (c *ChatCompletionsStreamResponseChoiceDelta) GetContentString() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ChatCompletionsStreamResponseChoiceDelta) GetReasoningContent() string {
|
func (c *ChatCompletionsStreamResponseChoiceDelta) GetReasoningContent() string {
|
||||||
if c.ReasoningContent == nil {
|
if c.ReasoningContent == nil && c.Reasoning == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return *c.ReasoningContent
|
if c.ReasoningContent != nil {
|
||||||
|
return *c.ReasoningContent
|
||||||
|
}
|
||||||
|
return *c.Reasoning
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ChatCompletionsStreamResponseChoiceDelta) SetReasoningContent(s string) {
|
func (c *ChatCompletionsStreamResponseChoiceDelta) SetReasoningContent(s string) {
|
||||||
c.ReasoningContent = &s
|
c.ReasoningContent = &s
|
||||||
|
c.Reasoning = &s
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolCall struct {
|
type ToolCallResponse struct {
|
||||||
// Index is not nil only in chat completion chunk object
|
// Index is not nil only in chat completion chunk object
|
||||||
Index *int `json:"index,omitempty"`
|
Index *int `json:"index,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Type any `json:"type"`
|
Type any `json:"type"`
|
||||||
Function FunctionCall `json:"function"`
|
Function FunctionResponse `json:"function"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ToolCall) SetIndex(i int) {
|
func (c *ToolCallResponse) SetIndex(i int) {
|
||||||
c.Index = &i
|
c.Index = &i
|
||||||
}
|
}
|
||||||
|
|
||||||
type FunctionCall struct {
|
type FunctionResponse struct {
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
// call function with arguments in JSON format
|
// call function with arguments in JSON format
|
||||||
Parameters any `json:"parameters,omitempty"` // request
|
Parameters any `json:"parameters,omitempty"` // request
|
||||||
Arguments string `json:"arguments,omitempty"`
|
Arguments string `json:"arguments"` // response
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatCompletionsStreamResponse struct {
|
type ChatCompletionsStreamResponse struct {
|
||||||
@@ -120,6 +116,20 @@ type ChatCompletionsStreamResponse struct {
|
|||||||
Usage *Usage `json:"usage"`
|
Usage *Usage `json:"usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ChatCompletionsStreamResponse) IsToolCall() bool {
|
||||||
|
if len(c.Choices) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(c.Choices[0].Delta.ToolCalls) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ChatCompletionsStreamResponse) GetFirstToolCall() *ToolCallResponse {
|
||||||
|
if c.IsToolCall() {
|
||||||
|
return &c.Choices[0].Delta.ToolCalls[0]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ChatCompletionsStreamResponse) Copy() *ChatCompletionsStreamResponse {
|
func (c *ChatCompletionsStreamResponse) Copy() *ChatCompletionsStreamResponse {
|
||||||
choices := make([]ChatCompletionsStreamResponseChoice, len(c.Choices))
|
choices := make([]ChatCompletionsStreamResponseChoice, len(c.Choices))
|
||||||
copy(choices, c.Choices)
|
copy(choices, c.Choices)
|
||||||
@@ -158,9 +168,95 @@ type CompletionsStreamResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
CompletionTokens int `json:"completion_tokens"`
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
TotalTokens int `json:"total_tokens"`
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens,omitempty"`
|
||||||
|
|
||||||
PromptTokensDetails InputTokenDetails `json:"prompt_tokens_details"`
|
PromptTokensDetails InputTokenDetails `json:"prompt_tokens_details"`
|
||||||
CompletionTokenDetails OutputTokenDetails `json:"completion_tokens_details"`
|
CompletionTokenDetails OutputTokenDetails `json:"completion_tokens_details"`
|
||||||
|
InputTokens int `json:"input_tokens"`
|
||||||
|
OutputTokens int `json:"output_tokens"`
|
||||||
|
InputTokensDetails *InputTokenDetails `json:"input_tokens_details"`
|
||||||
|
// OpenRouter Params
|
||||||
|
Cost float64 `json:"cost,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputTokenDetails struct {
|
||||||
|
CachedTokens int `json:"cached_tokens"`
|
||||||
|
CachedCreationTokens int `json:"-"`
|
||||||
|
TextTokens int `json:"text_tokens"`
|
||||||
|
AudioTokens int `json:"audio_tokens"`
|
||||||
|
ImageTokens int `json:"image_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputTokenDetails struct {
|
||||||
|
TextTokens int `json:"text_tokens"`
|
||||||
|
AudioTokens int `json:"audio_tokens"`
|
||||||
|
ReasoningTokens int `json:"reasoning_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIResponsesResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
CreatedAt int `json:"created_at"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error *OpenAIError `json:"error,omitempty"`
|
||||||
|
IncompleteDetails *IncompleteDetails `json:"incomplete_details,omitempty"`
|
||||||
|
Instructions string `json:"instructions"`
|
||||||
|
MaxOutputTokens int `json:"max_output_tokens"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Output []ResponsesOutput `json:"output"`
|
||||||
|
ParallelToolCalls bool `json:"parallel_tool_calls"`
|
||||||
|
PreviousResponseID string `json:"previous_response_id"`
|
||||||
|
Reasoning *Reasoning `json:"reasoning"`
|
||||||
|
Store bool `json:"store"`
|
||||||
|
Temperature float64 `json:"temperature"`
|
||||||
|
ToolChoice string `json:"tool_choice"`
|
||||||
|
Tools []ResponsesToolsCall `json:"tools"`
|
||||||
|
TopP float64 `json:"top_p"`
|
||||||
|
Truncation string `json:"truncation"`
|
||||||
|
Usage *Usage `json:"usage"`
|
||||||
|
User json.RawMessage `json:"user"`
|
||||||
|
Metadata json.RawMessage `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncompleteDetails struct {
|
||||||
|
Reasoning string `json:"reasoning"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponsesOutput struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content []ResponsesOutputContent `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponsesOutputContent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Annotations []interface{} `json:"annotations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
BuildInToolWebSearchPreview = "web_search_preview"
|
||||||
|
BuildInToolFileSearch = "file_search"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BuildInCallWebSearchCall = "web_search_call"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResponsesOutputTypeItemAdded = "response.output_item.added"
|
||||||
|
ResponsesOutputTypeItemDone = "response.output_item.done"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponsesStreamResponse 用于处理 /v1/responses 流式响应
|
||||||
|
type ResponsesStreamResponse struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Response *OpenAIResponsesResponse `json:"response,omitempty"`
|
||||||
|
Delta string `json:"delta,omitempty"`
|
||||||
|
Item *ResponsesOutput `json:"item,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
type OpenAIModelPermission struct {
|
import "one-api/constant"
|
||||||
Id string `json:"id"`
|
|
||||||
Object string `json:"object"`
|
|
||||||
Created int `json:"created"`
|
|
||||||
AllowCreateEngine bool `json:"allow_create_engine"`
|
|
||||||
AllowSampling bool `json:"allow_sampling"`
|
|
||||||
AllowLogprobs bool `json:"allow_logprobs"`
|
|
||||||
AllowSearchIndices bool `json:"allow_search_indices"`
|
|
||||||
AllowView bool `json:"allow_view"`
|
|
||||||
AllowFineTuning bool `json:"allow_fine_tuning"`
|
|
||||||
Organization string `json:"organization"`
|
|
||||||
Group *string `json:"group"`
|
|
||||||
IsBlocking bool `json:"is_blocking"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpenAIModels struct {
|
type OpenAIModels struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Object string `json:"object"`
|
Object string `json:"object"`
|
||||||
Created int `json:"created"`
|
Created int `json:"created"`
|
||||||
OwnedBy string `json:"owned_by"`
|
OwnedBy string `json:"owned_by"`
|
||||||
Permission []OpenAIModelPermission `json:"permission"`
|
SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
|
||||||
Root string `json:"root"`
|
|
||||||
Parent *string `json:"parent"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
38
dto/ratio_sync.go
Normal file
38
dto/ratio_sync.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type UpstreamDTO struct {
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
BaseURL string `json:"base_url" binding:"required"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpstreamRequest struct {
|
||||||
|
ChannelIDs []int64 `json:"channel_ids"`
|
||||||
|
Upstreams []UpstreamDTO `json:"upstreams"`
|
||||||
|
Timeout int `json:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResult 上游测试连通性结果
|
||||||
|
type TestResult struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DifferenceItem 差异项
|
||||||
|
// Current 为本地值,可能为 nil
|
||||||
|
// Upstreams 为各渠道的上游值,具体数值 / "same" / nil
|
||||||
|
|
||||||
|
type DifferenceItem struct {
|
||||||
|
Current interface{} `json:"current"`
|
||||||
|
Upstreams map[string]interface{} `json:"upstreams"`
|
||||||
|
Confidence map[string]bool `json:"confidence"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncableChannel struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
BaseURL string `json:"base_url"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
@@ -43,18 +43,6 @@ type RealtimeUsage struct {
|
|||||||
OutputTokenDetails OutputTokenDetails `json:"output_token_details"`
|
OutputTokenDetails OutputTokenDetails `json:"output_token_details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputTokenDetails struct {
|
|
||||||
CachedTokens int `json:"cached_tokens"`
|
|
||||||
TextTokens int `json:"text_tokens"`
|
|
||||||
AudioTokens int `json:"audio_tokens"`
|
|
||||||
ImageTokens int `json:"image_tokens"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputTokenDetails struct {
|
|
||||||
TextTokens int `json:"text_tokens"`
|
|
||||||
AudioTokens int `json:"audio_tokens"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RealtimeSession struct {
|
type RealtimeSession struct {
|
||||||
Modalities []string `json:"modalities"`
|
Modalities []string `json:"modalities"`
|
||||||
Instructions string `json:"instructions"`
|
Instructions string `json:"instructions"`
|
||||||
|
|||||||
@@ -4,19 +4,30 @@ type RerankRequest struct {
|
|||||||
Documents []any `json:"documents"`
|
Documents []any `json:"documents"`
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
TopN int `json:"top_n"`
|
TopN int `json:"top_n,omitempty"`
|
||||||
ReturnDocuments bool `json:"return_documents,omitempty"`
|
ReturnDocuments *bool `json:"return_documents,omitempty"`
|
||||||
MaxChunkPerDoc int `json:"max_chunk_per_doc,omitempty"`
|
MaxChunkPerDoc int `json:"max_chunk_per_doc,omitempty"`
|
||||||
OverLapTokens int `json:"overlap_tokens,omitempty"`
|
OverLapTokens int `json:"overlap_tokens,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RerankResponseDocument struct {
|
func (r *RerankRequest) GetReturnDocuments() bool {
|
||||||
|
if r.ReturnDocuments == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return *r.ReturnDocuments
|
||||||
|
}
|
||||||
|
|
||||||
|
type RerankResponseResult struct {
|
||||||
Document any `json:"document,omitempty"`
|
Document any `json:"document,omitempty"`
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
RelevanceScore float64 `json:"relevance_score"`
|
RelevanceScore float64 `json:"relevance_score"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RerankResponse struct {
|
type RerankDocument struct {
|
||||||
Results []RerankResponseDocument `json:"results"`
|
Text any `json:"text"`
|
||||||
Usage Usage `json:"usage"`
|
}
|
||||||
|
|
||||||
|
type RerankResponse struct {
|
||||||
|
Results []RerankResponseResult `json:"results"`
|
||||||
|
Usage Usage `json:"usage"`
|
||||||
}
|
}
|
||||||
|
|||||||
16
dto/user_settings.go
Normal file
16
dto/user_settings.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type UserSetting struct {
|
||||||
|
NotifyType string `json:"notify_type,omitempty"` // QuotaWarningType 额度预警类型
|
||||||
|
QuotaWarningThreshold float64 `json:"quota_warning_threshold,omitempty"` // QuotaWarningThreshold 额度预警阈值
|
||||||
|
WebhookUrl string `json:"webhook_url,omitempty"` // WebhookUrl webhook地址
|
||||||
|
WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥
|
||||||
|
NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址
|
||||||
|
AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
|
||||||
|
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
NotifyTypeEmail = "email" // Email 邮件
|
||||||
|
NotifyTypeWebhook = "webhook" // Webhook
|
||||||
|
)
|
||||||
47
dto/video.go
Normal file
47
dto/video.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type VideoRequest struct {
|
||||||
|
Model string `json:"model,omitempty" example:"kling-v1"` // Model/style ID
|
||||||
|
Prompt string `json:"prompt,omitempty" example:"宇航员站起身走了"` // Text prompt
|
||||||
|
Image string `json:"image,omitempty" example:"https://h2.inkwai.com/bs2/upload-ylab-stunt/se/ai_portal_queue_mmu_image_upscale_aiweb/3214b798-e1b4-4b00-b7af-72b5b0417420_raw_image_0.jpg"` // Image input (URL/Base64)
|
||||||
|
Duration float64 `json:"duration" example:"5.0"` // Video duration (seconds)
|
||||||
|
Width int `json:"width" example:"512"` // Video width
|
||||||
|
Height int `json:"height" example:"512"` // Video height
|
||||||
|
Fps int `json:"fps,omitempty" example:"30"` // Video frame rate
|
||||||
|
Seed int `json:"seed,omitempty" example:"20231234"` // Random seed
|
||||||
|
N int `json:"n,omitempty" example:"1"` // Number of videos to generate
|
||||||
|
ResponseFormat string `json:"response_format,omitempty" example:"url"` // Response format
|
||||||
|
User string `json:"user,omitempty" example:"user-1234"` // User identifier
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"` // Vendor-specific/custom params (e.g. negative_prompt, style, quality_level, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoResponse 视频生成提交任务后的响应
|
||||||
|
type VideoResponse struct {
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoTaskResponse 查询视频生成任务状态的响应
|
||||||
|
type VideoTaskResponse struct {
|
||||||
|
TaskId string `json:"task_id" example:"abcd1234efgh"` // 任务ID
|
||||||
|
Status string `json:"status" example:"succeeded"` // 任务状态
|
||||||
|
Url string `json:"url,omitempty"` // 视频资源URL(成功时)
|
||||||
|
Format string `json:"format,omitempty" example:"mp4"` // 视频格式
|
||||||
|
Metadata *VideoTaskMetadata `json:"metadata,omitempty"` // 结果元数据
|
||||||
|
Error *VideoTaskError `json:"error,omitempty"` // 错误信息(失败时)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoTaskMetadata 视频任务元数据
|
||||||
|
type VideoTaskMetadata struct {
|
||||||
|
Duration float64 `json:"duration" example:"5.0"` // 实际生成的视频时长
|
||||||
|
Fps int `json:"fps" example:"30"` // 实际帧率
|
||||||
|
Width int `json:"width" example:"512"` // 实际宽度
|
||||||
|
Height int `json:"height" example:"512"` // 实际高度
|
||||||
|
Seed int `json:"seed" example:"20231234"` // 使用的随机种子
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoTaskError 视频任务错误信息
|
||||||
|
type VideoTaskError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
19
go.mod
19
go.mod
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
|
||||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
|
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
|
||||||
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
|
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
|
||||||
|
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994
|
||||||
github.com/gin-contrib/cors v1.7.2
|
github.com/gin-contrib/cors v1.7.2
|
||||||
github.com/gin-contrib/gzip v0.0.6
|
github.com/gin-contrib/gzip v0.0.6
|
||||||
github.com/gin-contrib/sessions v0.0.5
|
github.com/gin-contrib/sessions v0.0.5
|
||||||
@@ -22,15 +23,16 @@ require (
|
|||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/jinzhu/copier v0.4.0
|
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkoukk/tiktoken-go v0.1.7
|
|
||||||
github.com/samber/lo v1.39.0
|
github.com/samber/lo v1.39.0
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
golang.org/x/crypto v0.27.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
|
github.com/tiktoken-go/tokenizer v0.6.2
|
||||||
|
golang.org/x/crypto v0.35.0
|
||||||
golang.org/x/image v0.23.0
|
golang.org/x/image v0.23.0
|
||||||
golang.org/x/net v0.28.0
|
golang.org/x/net v0.35.0
|
||||||
|
golang.org/x/sync v0.11.0
|
||||||
gorm.io/driver/mysql v1.4.3
|
gorm.io/driver/mysql v1.4.3
|
||||||
gorm.io/driver/postgres v1.5.2
|
gorm.io/driver/postgres v1.5.2
|
||||||
gorm.io/gorm v1.25.2
|
gorm.io/gorm v1.25.2
|
||||||
@@ -48,7 +50,7 @@ require (
|
|||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
@@ -56,9 +58,11 @@ require (
|
|||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/gorilla/sessions v1.2.1 // indirect
|
github.com/gorilla/sessions v1.2.1 // indirect
|
||||||
@@ -84,9 +88,8 @@ require (
|
|||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
golang.org/x/arch v0.12.0 // indirect
|
golang.org/x/arch v0.12.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/sys v0.27.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
|
|||||||
42
go.sum
42
go.sum
@@ -1,5 +1,7 @@
|
|||||||
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
|
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
|
||||||
github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
|
github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
|
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
|
||||||
@@ -38,8 +40,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 h1:aQYWswi+hRL2zJqGacdCZx32XjKYV8ApXFGntw79XAM=
|
||||||
|
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
@@ -83,6 +87,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
|
|||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
@@ -97,8 +103,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
@@ -117,8 +123,6 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
|||||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
|
||||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
@@ -169,8 +173,6 @@ github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h
|
|||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
|
|
||||||
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
@@ -183,6 +185,8 @@ github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
|||||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -197,6 +201,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tiktoken-go/tokenizer v0.6.2 h1:t0GN2DvcUZSFWT/62YOgoqb10y7gSXBGs0A+4VCQK+g=
|
||||||
|
github.com/tiktoken-go/tokenizer v0.6.2/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=
|
||||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
@@ -217,18 +223,18 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
|
|||||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
||||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -239,14 +245,14 @@ golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
|||||||
1041
i18n/zh-cn.json
Normal file
1041
i18n/zh-cn.json
Normal file
File diff suppressed because it is too large
Load Diff
106
main.go
106
main.go
@@ -12,6 +12,7 @@ import (
|
|||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/router"
|
"one-api/router"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
|
"one-api/setting/ratio_setting"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -31,14 +32,13 @@ var buildFS embed.FS
|
|||||||
var indexPage []byte
|
var indexPage []byte
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := godotenv.Load(".env")
|
|
||||||
|
err := InitResources()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysLog("Support for .env file is disabled")
|
common.FatalLog("failed to initialize resources: " + err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
common.LoadEnv()
|
|
||||||
|
|
||||||
common.SetupLogger()
|
|
||||||
common.SysLog("New API " + common.Version + " started")
|
common.SysLog("New API " + common.Version + " started")
|
||||||
if os.Getenv("GIN_MODE") != "debug" {
|
if os.Getenv("GIN_MODE") != "debug" {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@@ -46,16 +46,7 @@ func main() {
|
|||||||
if common.DebugEnabled {
|
if common.DebugEnabled {
|
||||||
common.SysLog("running in debug mode")
|
common.SysLog("running in debug mode")
|
||||||
}
|
}
|
||||||
// Initialize SQL Database
|
|
||||||
err = model.InitDB()
|
|
||||||
if err != nil {
|
|
||||||
common.FatalLog("failed to initialize database: " + err.Error())
|
|
||||||
}
|
|
||||||
// Initialize SQL Database
|
|
||||||
err = model.InitLogDB()
|
|
||||||
if err != nil {
|
|
||||||
common.FatalLog("failed to initialize database: " + err.Error())
|
|
||||||
}
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := model.CloseDB()
|
err := model.CloseDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,16 +54,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Initialize Redis
|
|
||||||
err = common.InitRedisClient()
|
|
||||||
if err != nil {
|
|
||||||
common.FatalLog("failed to initialize Redis: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize constants
|
|
||||||
constant.InitEnv()
|
|
||||||
// Initialize options
|
|
||||||
model.InitOptionMap()
|
|
||||||
if common.RedisEnabled {
|
if common.RedisEnabled {
|
||||||
// for compatibility with old versions
|
// for compatibility with old versions
|
||||||
common.MemoryCacheEnabled = true
|
common.MemoryCacheEnabled = true
|
||||||
@@ -80,13 +61,28 @@ func main() {
|
|||||||
if common.MemoryCacheEnabled {
|
if common.MemoryCacheEnabled {
|
||||||
common.SysLog("memory cache enabled")
|
common.SysLog("memory cache enabled")
|
||||||
common.SysError(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
|
common.SysError(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
|
||||||
model.InitChannelCache()
|
|
||||||
}
|
// Add panic recovery and retry for InitChannelCache
|
||||||
if common.MemoryCacheEnabled {
|
func() {
|
||||||
go model.SyncOptions(common.SyncFrequency)
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
common.SysError(fmt.Sprintf("InitChannelCache panic: %v, retrying once", r))
|
||||||
|
// Retry once
|
||||||
|
_, _, fixErr := model.FixAbility()
|
||||||
|
if fixErr != nil {
|
||||||
|
common.FatalLog(fmt.Sprintf("InitChannelCache failed: %s", fixErr.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
model.InitChannelCache()
|
||||||
|
}()
|
||||||
|
|
||||||
go model.SyncChannelCache(common.SyncFrequency)
|
go model.SyncChannelCache(common.SyncFrequency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 热更新配置
|
||||||
|
go model.SyncOptions(common.SyncFrequency)
|
||||||
|
|
||||||
// 数据看板
|
// 数据看板
|
||||||
go model.UpdateQuotaData()
|
go model.UpdateQuotaData()
|
||||||
|
|
||||||
@@ -126,8 +122,6 @@ func main() {
|
|||||||
common.SysLog("pprof enabled")
|
common.SysLog("pprof enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
service.InitTokenEncoders()
|
|
||||||
|
|
||||||
// Initialize HTTP server
|
// Initialize HTTP server
|
||||||
server := gin.New()
|
server := gin.New()
|
||||||
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
|
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
|
||||||
@@ -164,3 +158,53 @@ func main() {
|
|||||||
common.FatalLog("failed to start HTTP server: " + err.Error())
|
common.FatalLog("failed to start HTTP server: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitResources() error {
|
||||||
|
// Initialize resources here if needed
|
||||||
|
// This is a placeholder function for future resource initialization
|
||||||
|
err := godotenv.Load(".env")
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("未找到 .env 文件,使用默认环境变量,如果需要,请创建 .env 文件并设置相关变量")
|
||||||
|
common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载环境变量
|
||||||
|
common.InitEnv()
|
||||||
|
|
||||||
|
common.SetupLogger()
|
||||||
|
|
||||||
|
// Initialize model settings
|
||||||
|
ratio_setting.InitRatioSettings()
|
||||||
|
|
||||||
|
service.InitHttpClient()
|
||||||
|
|
||||||
|
service.InitTokenEncoders()
|
||||||
|
|
||||||
|
// Initialize SQL Database
|
||||||
|
err = model.InitDB()
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog("failed to initialize database: " + err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
model.CheckSetup()
|
||||||
|
|
||||||
|
// Initialize options, should after model.InitDB()
|
||||||
|
model.InitOptionMap()
|
||||||
|
|
||||||
|
// 初始化模型
|
||||||
|
model.GetPricing()
|
||||||
|
|
||||||
|
// Initialize SQL Database
|
||||||
|
err = model.InitLogDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Redis
|
||||||
|
err = common.InitRedisClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
2
makefile
2
makefile
@@ -7,7 +7,7 @@ all: build-frontend start-backend
|
|||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
@echo "Building frontend..."
|
@echo "Building frontend..."
|
||||||
@cd $(FRONTEND_DIR) && npm install && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) npm run build
|
@cd $(FRONTEND_DIR) && bun install && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
|
||||||
|
|
||||||
start-backend:
|
start-backend:
|
||||||
@echo "Starting backend dev server..."
|
@echo "Starting backend dev server..."
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validUserInfo(username string, role int) bool {
|
func validUserInfo(username string, role int) bool {
|
||||||
@@ -174,6 +175,26 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.Request.Header.Set("Authorization", "Bearer "+key)
|
c.Request.Header.Set("Authorization", "Bearer "+key)
|
||||||
}
|
}
|
||||||
|
// 检查path包含/v1/messages
|
||||||
|
if strings.Contains(c.Request.URL.Path, "/v1/messages") {
|
||||||
|
// 从x-api-key中获取key
|
||||||
|
key := c.Request.Header.Get("x-api-key")
|
||||||
|
if key != "" {
|
||||||
|
c.Request.Header.Set("Authorization", "Bearer "+key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// gemini api 从query中获取key
|
||||||
|
if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models/") || strings.HasPrefix(c.Request.URL.Path, "/v1/models/") {
|
||||||
|
skKey := c.Query("key")
|
||||||
|
if skKey != "" {
|
||||||
|
c.Request.Header.Set("Authorization", "Bearer "+skKey)
|
||||||
|
}
|
||||||
|
// 从x-goog-api-key header中获取key
|
||||||
|
xGoogKey := c.Request.Header.Get("x-goog-api-key")
|
||||||
|
if xGoogKey != "" {
|
||||||
|
c.Request.Header.Set("Authorization", "Bearer "+xGoogKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
key := c.Request.Header.Get("Authorization")
|
key := c.Request.Header.Get("Authorization")
|
||||||
parts := make([]string, 0)
|
parts := make([]string, 0)
|
||||||
key = strings.TrimPrefix(key, "Bearer ")
|
key = strings.TrimPrefix(key, "Bearer ")
|
||||||
@@ -199,15 +220,19 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
abortWithOpenAiMessage(c, http.StatusUnauthorized, err.Error())
|
abortWithOpenAiMessage(c, http.StatusUnauthorized, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userEnabled, err := model.IsUserEnabled(token.UserId, false)
|
userCache, err := model.GetUserCache(token.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
abortWithOpenAiMessage(c, http.StatusInternalServerError, err.Error())
|
abortWithOpenAiMessage(c, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
userEnabled := userCache.Status == common.UserStatusEnabled
|
||||||
if !userEnabled {
|
if !userEnabled {
|
||||||
abortWithOpenAiMessage(c, http.StatusForbidden, "用户已被封禁")
|
abortWithOpenAiMessage(c, http.StatusForbidden, "用户已被封禁")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userCache.WriteContext(c)
|
||||||
|
|
||||||
c.Set("id", token.UserId)
|
c.Set("id", token.UserId)
|
||||||
c.Set("token_id", token.Id)
|
c.Set("token_id", token.Id)
|
||||||
c.Set("token_key", token.Key)
|
c.Set("token_key", token.Key)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
relayconstant "one-api/relay/constant"
|
relayconstant "one-api/relay/constant"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/ratio_setting"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,7 +25,7 @@ type ModelRequest struct {
|
|||||||
|
|
||||||
func Distribute() func(c *gin.Context) {
|
func Distribute() func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
allowIpsMap := c.GetStringMap("allow_ips")
|
allowIpsMap := common.GetContextKeyStringMap(c, constant.ContextKeyTokenAllowIps)
|
||||||
if len(allowIpsMap) != 0 {
|
if len(allowIpsMap) != 0 {
|
||||||
clientIp := c.ClientIP()
|
clientIp := c.ClientIP()
|
||||||
if _, ok := allowIpsMap[clientIp]; !ok {
|
if _, ok := allowIpsMap[clientIp]; !ok {
|
||||||
@@ -32,16 +33,15 @@ func Distribute() func(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userId := c.GetInt("id")
|
|
||||||
var channel *model.Channel
|
var channel *model.Channel
|
||||||
channelId, ok := c.Get("specific_channel_id")
|
channelId, ok := common.GetContextKey(c, constant.ContextKeyTokenSpecificChannelId)
|
||||||
modelRequest, shouldSelectChannel, err := getModelRequest(c)
|
modelRequest, shouldSelectChannel, err := getModelRequest(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
abortWithOpenAiMessage(c, http.StatusBadRequest, "Invalid request, "+err.Error())
|
abortWithOpenAiMessage(c, http.StatusBadRequest, "Invalid request, "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userGroup, _ := model.GetUserGroup(userId, false)
|
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
|
||||||
tokenGroup := c.GetString("token_group")
|
tokenGroup := common.GetContextKeyString(c, constant.ContextKeyTokenGroup)
|
||||||
if tokenGroup != "" {
|
if tokenGroup != "" {
|
||||||
// check common.UserUsableGroups[userGroup]
|
// check common.UserUsableGroups[userGroup]
|
||||||
if _, ok := setting.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
|
if _, ok := setting.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
|
||||||
@@ -49,13 +49,15 @@ func Distribute() func(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check group in common.GroupRatio
|
// check group in common.GroupRatio
|
||||||
if !setting.ContainsGroupRatio(tokenGroup) {
|
if !ratio_setting.ContainsGroupRatio(tokenGroup) {
|
||||||
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
|
if tokenGroup != "auto" {
|
||||||
return
|
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
userGroup = tokenGroup
|
userGroup = tokenGroup
|
||||||
}
|
}
|
||||||
c.Set("group", userGroup)
|
common.SetContextKey(c, constant.ContextKeyUsingGroup, userGroup)
|
||||||
if ok {
|
if ok {
|
||||||
id, err := strconv.Atoi(channelId.(string))
|
id, err := strconv.Atoi(channelId.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,9 +76,9 @@ func Distribute() func(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
// Select a channel for the user
|
// Select a channel for the user
|
||||||
// check token model mapping
|
// check token model mapping
|
||||||
modelLimitEnable := c.GetBool("token_model_limit_enabled")
|
modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled)
|
||||||
if modelLimitEnable {
|
if modelLimitEnable {
|
||||||
s, ok := c.Get("token_model_limit")
|
s, ok := common.GetContextKey(c, constant.ContextKeyTokenModelLimit)
|
||||||
var tokenModelLimit map[string]bool
|
var tokenModelLimit map[string]bool
|
||||||
if ok {
|
if ok {
|
||||||
tokenModelLimit = s.(map[string]bool)
|
tokenModelLimit = s.(map[string]bool)
|
||||||
@@ -96,9 +98,14 @@ func Distribute() func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if shouldSelectChannel {
|
if shouldSelectChannel {
|
||||||
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model, 0)
|
var selectGroup string
|
||||||
|
channel, selectGroup, err = model.CacheGetRandomSatisfiedChannel(c, userGroup, modelRequest.Model, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
|
showGroup := userGroup
|
||||||
|
if userGroup == "auto" {
|
||||||
|
showGroup = fmt.Sprintf("auto(%s)", selectGroup)
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", showGroup, modelRequest.Model)
|
||||||
// 如果错误,但是渠道不为空,说明是数据库一致性问题
|
// 如果错误,但是渠道不为空,说明是数据库一致性问题
|
||||||
if channel != nil {
|
if channel != nil {
|
||||||
common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
|
common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
|
||||||
@@ -114,7 +121,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.Set(constant.ContextKeyRequestStartTime, time.Now())
|
common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
|
||||||
SetupContextForSelectedChannel(c, channel, modelRequest.Model)
|
SetupContextForSelectedChannel(c, channel, modelRequest.Model)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
@@ -163,7 +170,34 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
|||||||
}
|
}
|
||||||
c.Set("platform", string(constant.TaskPlatformSuno))
|
c.Set("platform", string(constant.TaskPlatformSuno))
|
||||||
c.Set("relay_mode", relayMode)
|
c.Set("relay_mode", relayMode)
|
||||||
} else if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") {
|
} else if strings.Contains(c.Request.URL.Path, "/v1/video/generations") {
|
||||||
|
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||||
|
var platform string
|
||||||
|
var relayMode int
|
||||||
|
if strings.HasPrefix(modelRequest.Model, "jimeng") {
|
||||||
|
platform = string(constant.TaskPlatformJimeng)
|
||||||
|
relayMode = relayconstant.Path2RelayJimeng(c.Request.Method, c.Request.URL.Path)
|
||||||
|
if relayMode == relayconstant.RelayModeJimengFetchByID {
|
||||||
|
shouldSelectChannel = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
platform = string(constant.TaskPlatformKling)
|
||||||
|
relayMode = relayconstant.Path2RelayKling(c.Request.Method, c.Request.URL.Path)
|
||||||
|
if relayMode == relayconstant.RelayModeKlingFetchByID {
|
||||||
|
shouldSelectChannel = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Set("platform", platform)
|
||||||
|
c.Set("relay_mode", relayMode)
|
||||||
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models/") || strings.HasPrefix(c.Request.URL.Path, "/v1/models/") {
|
||||||
|
// Gemini API 路径处理: /v1beta/models/gemini-2.0-flash:generateContent
|
||||||
|
relayMode := relayconstant.RelayModeGemini
|
||||||
|
modelName := extractModelNameFromGeminiPath(c.Request.URL.Path)
|
||||||
|
if modelName != "" {
|
||||||
|
modelRequest.Model = modelName
|
||||||
|
}
|
||||||
|
c.Set("relay_mode", relayMode)
|
||||||
|
} else if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") && !strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
|
||||||
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -185,6 +219,8 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
|||||||
}
|
}
|
||||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
|
||||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "dall-e")
|
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "dall-e")
|
||||||
|
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
|
||||||
|
modelRequest.Model = common.GetStringIfEmpty(c.PostForm("model"), "gpt-image-1")
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
|
if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
|
||||||
relayMode := relayconstant.RelayModeAudioSpeech
|
relayMode := relayconstant.RelayModeAudioSpeech
|
||||||
@@ -211,8 +247,10 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
|||||||
}
|
}
|
||||||
c.Set("channel_id", channel.Id)
|
c.Set("channel_id", channel.Id)
|
||||||
c.Set("channel_name", channel.Name)
|
c.Set("channel_name", channel.Name)
|
||||||
c.Set("channel_type", channel.Type)
|
common.SetContextKey(c, constant.ContextKeyChannelType, channel.Type)
|
||||||
c.Set("channel_setting", channel.GetSetting())
|
c.Set("channel_create_time", channel.CreatedTime)
|
||||||
|
common.SetContextKey(c, constant.ContextKeyChannelSetting, channel.GetSetting())
|
||||||
|
c.Set("param_override", channel.GetParamOverride())
|
||||||
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
|
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
|
||||||
c.Set("channel_organization", *channel.OpenAIOrganization)
|
c.Set("channel_organization", *channel.OpenAIOrganization)
|
||||||
}
|
}
|
||||||
@@ -220,22 +258,52 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
|||||||
c.Set("model_mapping", channel.GetModelMapping())
|
c.Set("model_mapping", channel.GetModelMapping())
|
||||||
c.Set("status_code_mapping", channel.GetStatusCodeMapping())
|
c.Set("status_code_mapping", channel.GetStatusCodeMapping())
|
||||||
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
||||||
c.Set("base_url", channel.GetBaseURL())
|
common.SetContextKey(c, constant.ContextKeyBaseUrl, channel.GetBaseURL())
|
||||||
// TODO: api_version统一
|
// TODO: api_version统一
|
||||||
switch channel.Type {
|
switch channel.Type {
|
||||||
case common.ChannelTypeAzure:
|
case constant.ChannelTypeAzure:
|
||||||
c.Set("api_version", channel.Other)
|
c.Set("api_version", channel.Other)
|
||||||
case common.ChannelTypeVertexAi:
|
case constant.ChannelTypeVertexAi:
|
||||||
c.Set("region", channel.Other)
|
c.Set("region", channel.Other)
|
||||||
case common.ChannelTypeXunfei:
|
case constant.ChannelTypeXunfei:
|
||||||
c.Set("api_version", channel.Other)
|
c.Set("api_version", channel.Other)
|
||||||
case common.ChannelTypeGemini:
|
case constant.ChannelTypeGemini:
|
||||||
c.Set("api_version", channel.Other)
|
c.Set("api_version", channel.Other)
|
||||||
case common.ChannelTypeAli:
|
case constant.ChannelTypeAli:
|
||||||
c.Set("plugin", channel.Other)
|
c.Set("plugin", channel.Other)
|
||||||
case common.ChannelCloudflare:
|
case constant.ChannelCloudflare:
|
||||||
c.Set("api_version", channel.Other)
|
c.Set("api_version", channel.Other)
|
||||||
case common.ChannelTypeMokaAI:
|
case constant.ChannelTypeMokaAI:
|
||||||
c.Set("api_version", channel.Other)
|
c.Set("api_version", channel.Other)
|
||||||
|
case constant.ChannelTypeCoze:
|
||||||
|
c.Set("bot_id", channel.Other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractModelNameFromGeminiPath 从 Gemini API URL 路径中提取模型名
|
||||||
|
// 输入格式: /v1beta/models/gemini-2.0-flash:generateContent
|
||||||
|
// 输出: gemini-2.0-flash
|
||||||
|
func extractModelNameFromGeminiPath(path string) string {
|
||||||
|
// 查找 "/models/" 的位置
|
||||||
|
modelsPrefix := "/models/"
|
||||||
|
modelsIndex := strings.Index(path, modelsPrefix)
|
||||||
|
if modelsIndex == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 "/models/" 之后开始提取
|
||||||
|
startIndex := modelsIndex + len(modelsPrefix)
|
||||||
|
if startIndex >= len(path) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找 ":" 的位置,模型名在 ":" 之前
|
||||||
|
colonIndex := strings.Index(path[startIndex:], ":")
|
||||||
|
if colonIndex == -1 {
|
||||||
|
// 如果没有找到 ":",返回从 "/models/" 到路径结尾的部分
|
||||||
|
return path[startIndex:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回模型名部分
|
||||||
|
return path[startIndex : startIndex+colonIndex]
|
||||||
|
}
|
||||||
|
|||||||
62
middleware/jsrt/cfg.go
Normal file
62
middleware/jsrt/cfg.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package jsrt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Runtime 配置
|
||||||
|
type JSRuntimeConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
MaxVMCount int `json:"max_vm_count"`
|
||||||
|
ScriptTimeout time.Duration `json:"script_timeout"`
|
||||||
|
ScriptDir string `json:"script_dir"`
|
||||||
|
FetchTimeout time.Duration `json:"fetch_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
jsConfig = JSRuntimeConfig{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultScriptDir = "scripts/"
|
||||||
|
defaultScriptTimeout = 5 * time.Second
|
||||||
|
defaultFetchTimeout = 10 * time.Second
|
||||||
|
defaultMaxVMCount = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadCfg() {
|
||||||
|
if enabled := os.Getenv("JS_RUNTIME_ENABLED"); enabled != "" {
|
||||||
|
jsConfig.Enabled = enabled == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxCount := os.Getenv("JS_MAX_VM_COUNT"); maxCount != "" {
|
||||||
|
if count, err := strconv.Atoi(maxCount); err == nil && count > 0 {
|
||||||
|
jsConfig.MaxVMCount = count
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jsConfig.MaxVMCount = defaultMaxVMCount
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeout := os.Getenv("JS_SCRIPT_TIMEOUT"); timeout != "" {
|
||||||
|
if t, err := time.ParseDuration(timeout + "s"); err == nil && t > 0 {
|
||||||
|
jsConfig.ScriptTimeout = t
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jsConfig.ScriptTimeout = defaultScriptTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
if fetchTimeout := os.Getenv("JS_FETCH_TIMEOUT"); fetchTimeout != "" {
|
||||||
|
if t, err := time.ParseDuration(fetchTimeout + "s"); err == nil && t > 0 {
|
||||||
|
jsConfig.FetchTimeout = t
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jsConfig.FetchTimeout = defaultFetchTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
jsConfig.ScriptDir = os.Getenv("JS_SCRIPT_DIR")
|
||||||
|
if jsConfig.ScriptDir == "" {
|
||||||
|
jsConfig.ScriptDir = defaultScriptDir
|
||||||
|
}
|
||||||
|
}
|
||||||
69
middleware/jsrt/db.go
Normal file
69
middleware/jsrt/db.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package jsrt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dbQuery(db *gorm.DB, sql string, args ...any) []map[string]any {
|
||||||
|
if db == nil {
|
||||||
|
common.SysError("JS DB is nil")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Raw(sql, args...).Rows()
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("JS DB Query Error: " + err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
columns, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("JS DB Columns Error: " + err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]map[string]any, 0, 100)
|
||||||
|
for rows.Next() {
|
||||||
|
values := make([]any, len(columns))
|
||||||
|
valuePtrs := make([]any, len(columns))
|
||||||
|
for i := range values {
|
||||||
|
valuePtrs[i] = &values[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Scan(valuePtrs...); err != nil {
|
||||||
|
common.SysError("JS DB Scan Error: " + err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
row := make(map[string]any, len(columns))
|
||||||
|
for i, col := range columns {
|
||||||
|
val := values[i]
|
||||||
|
if b, ok := val.([]byte); ok {
|
||||||
|
row[col] = string(b)
|
||||||
|
} else {
|
||||||
|
row[col] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbExec(db *gorm.DB, sql string, args ...any) map[string]any {
|
||||||
|
if db == nil {
|
||||||
|
return map[string]any{
|
||||||
|
"rowsAffected": int64(0),
|
||||||
|
"error": "database is nil",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := db.Exec(sql, args...)
|
||||||
|
return map[string]any{
|
||||||
|
"rowsAffected": result.RowsAffected,
|
||||||
|
"error": result.Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
137
middleware/jsrt/fetch.go
Normal file
137
middleware/jsrt/fetch.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package jsrt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSFetchRequest struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
Body any `json:"body"`
|
||||||
|
Timeout int `json:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSFetchResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) fetch(url string, options ...any) *JSFetchResponse {
|
||||||
|
req := &JSFetchRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: url,
|
||||||
|
Headers: make(map[string]string),
|
||||||
|
Timeout: int(jsConfig.FetchTimeout.Seconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析选项
|
||||||
|
if len(options) > 0 && options[0] != nil {
|
||||||
|
if optMap, ok := options[0].(map[string]any); ok {
|
||||||
|
if method, exists := optMap["method"]; exists {
|
||||||
|
if methodStr, ok := method.(string); ok {
|
||||||
|
req.Method = strings.ToUpper(methodStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if headers, exists := optMap["headers"]; exists {
|
||||||
|
if headersMap, ok := headers.(map[string]any); ok {
|
||||||
|
for k, v := range headersMap {
|
||||||
|
if vStr, ok := v.(string); ok {
|
||||||
|
req.Headers[k] = vStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if body, exists := optMap["body"]; exists {
|
||||||
|
req.Body = body
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeout, exists := optMap["timeout"]; exists {
|
||||||
|
if timeoutNum, ok := timeout.(float64); ok {
|
||||||
|
req.Timeout = int(timeoutNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建HTTP请求
|
||||||
|
var bodyReader io.Reader
|
||||||
|
switch body := req.Body.(type) {
|
||||||
|
case string:
|
||||||
|
bodyReader = strings.NewReader(body)
|
||||||
|
case []byte:
|
||||||
|
bodyReader = bytes.NewReader(body)
|
||||||
|
case nil:
|
||||||
|
bodyReader = nil
|
||||||
|
default:
|
||||||
|
bodyBytes, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return &JSFetchResponse{
|
||||||
|
Error: fmt.Sprintf("Failed to marshal body: %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(bodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequest(req.Method, req.URL, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return &JSFetchResponse{
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
for k, v := range req.Headers {
|
||||||
|
httpReq.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认User-Agent
|
||||||
|
if httpReq.Header.Get("User-Agent") == "" {
|
||||||
|
httpReq.Header.Set("User-Agent", "JS-Runtime-Fetch/1.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建带超时的上下文
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Timeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
httpReq = httpReq.WithContext(ctx)
|
||||||
|
|
||||||
|
// 执行请求
|
||||||
|
resp, err := p.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return &JSFetchResponse{}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 读取响应体
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &JSFetchResponse{
|
||||||
|
Status: resp.StatusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建响应头
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
if len(v) > 0 {
|
||||||
|
headers[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &JSFetchResponse{
|
||||||
|
Status: resp.StatusCode,
|
||||||
|
Headers: headers,
|
||||||
|
Body: string(bodyBytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
570
middleware/jsrt/jsrt.go
Normal file
570
middleware/jsrt/jsrt.go
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
package jsrt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 池化
|
||||||
|
type JSRuntimePool struct {
|
||||||
|
pool chan *goja.Runtime
|
||||||
|
maxSize int
|
||||||
|
createFunc func() *goja.Runtime
|
||||||
|
scripts string
|
||||||
|
mu sync.RWMutex
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
jsRuntimePool *JSRuntimePool
|
||||||
|
jsPoolOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewJSRuntimePool(maxSize int) *JSRuntimePool {
|
||||||
|
// 创建HTTP客户端
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: jsConfig.FetchTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
},
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := &JSRuntimePool{
|
||||||
|
pool: make(chan *goja.Runtime, maxSize),
|
||||||
|
maxSize: maxSize,
|
||||||
|
scripts: "",
|
||||||
|
httpClient: httpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.createFunc = func() *goja.Runtime {
|
||||||
|
vm := goja.New()
|
||||||
|
pool.setupGlobals(vm)
|
||||||
|
pool.loadScripts(vm)
|
||||||
|
return vm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预创建VM
|
||||||
|
preCreate := min(maxSize/2, 4)
|
||||||
|
for range preCreate {
|
||||||
|
select {
|
||||||
|
case pool.pool <- pool.createFunc():
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) Get() *goja.Runtime {
|
||||||
|
select {
|
||||||
|
case vm := <-p.pool:
|
||||||
|
return vm
|
||||||
|
default:
|
||||||
|
return p.createFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) Put(vm *goja.Runtime) {
|
||||||
|
if vm == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case p.pool <- vm:
|
||||||
|
default:
|
||||||
|
// 池满,丢弃VM让GC回收
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) setupGlobals(vm *goja.Runtime) {
|
||||||
|
// console
|
||||||
|
console := vm.NewObject()
|
||||||
|
console.Set("log", func(args ...any) {
|
||||||
|
var strs []string
|
||||||
|
for _, arg := range args {
|
||||||
|
strs = append(strs, fmt.Sprintf("%v", arg))
|
||||||
|
}
|
||||||
|
common.SysLog("JS: " + strings.Join(strs, " "))
|
||||||
|
})
|
||||||
|
console.Set("error", func(args ...any) {
|
||||||
|
var strs []string
|
||||||
|
for _, arg := range args {
|
||||||
|
strs = append(strs, fmt.Sprintf("%v", arg))
|
||||||
|
}
|
||||||
|
common.SysError("JS: " + strings.Join(strs, " "))
|
||||||
|
})
|
||||||
|
console.Set("warn", func(args ...any) {
|
||||||
|
var strs []string
|
||||||
|
for _, arg := range args {
|
||||||
|
strs = append(strs, fmt.Sprintf("%v", arg))
|
||||||
|
}
|
||||||
|
common.SysError("JS WARN: " + strings.Join(strs, " "))
|
||||||
|
})
|
||||||
|
vm.Set("console", console)
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
jsonObj := vm.NewObject()
|
||||||
|
jsonObj.Set("parse", func(str string) any {
|
||||||
|
var result any
|
||||||
|
err := json.Unmarshal([]byte(str), &result)
|
||||||
|
if err != nil {
|
||||||
|
panic(vm.ToValue(err.Error()))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
jsonObj.Set("stringify", func(obj any) string {
|
||||||
|
data, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
panic(vm.ToValue(err.Error()))
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
})
|
||||||
|
vm.Set("JSON", jsonObj)
|
||||||
|
|
||||||
|
// fetch 实现
|
||||||
|
vm.Set("fetch", func(url string, options ...any) *JSFetchResponse {
|
||||||
|
return p.fetch(url, options...)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 数据库
|
||||||
|
setDB(vm, model.DB, "db")
|
||||||
|
setDB(vm, model.LOG_DB, "logdb")
|
||||||
|
|
||||||
|
// 定时器
|
||||||
|
vm.Set("setTimeout", func(fn func(), delay int) {
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||||
|
fn()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) loadScripts(vm *goja.Runtime) {
|
||||||
|
p.mu.RLock()
|
||||||
|
defer p.mu.RUnlock()
|
||||||
|
|
||||||
|
// 如果已经缓存了合并的脚本,直接使用
|
||||||
|
if p.scripts != "" {
|
||||||
|
if _, err := vm.RunString(p.scripts); err != nil {
|
||||||
|
common.SysError("Failed to load cached scripts: " + err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首次加载时,读取 scripts/ 文件夹中的所有脚本
|
||||||
|
p.mu.RUnlock()
|
||||||
|
p.mu.Lock()
|
||||||
|
defer func() {
|
||||||
|
p.mu.Unlock()
|
||||||
|
p.mu.RLock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if p.scripts != "" {
|
||||||
|
if _, err := vm.RunString(p.scripts); err != nil {
|
||||||
|
common.SysError("Failed to load cached scripts: " + err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取所有脚本文件
|
||||||
|
var combinedScript strings.Builder
|
||||||
|
scriptDir := jsConfig.ScriptDir
|
||||||
|
|
||||||
|
// 检查目录是否存在
|
||||||
|
if _, err := os.Stat(scriptDir); os.IsNotExist(err) {
|
||||||
|
common.SysLog("Scripts directory does not exist: " + scriptDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取目录中的所有 .js 文件
|
||||||
|
files, err := filepath.Glob(filepath.Join(scriptDir, "*.js"))
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("Failed to read scripts directory: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 0 {
|
||||||
|
common.SysLog("No JavaScript files found in: " + scriptDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按文件名排序以确保加载顺序一致
|
||||||
|
for _, file := range files {
|
||||||
|
content, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("Failed to read script file " + file + ": " + err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文件注释和内容
|
||||||
|
combinedScript.WriteString("// File: " + filepath.Base(file) + "\n")
|
||||||
|
combinedScript.WriteString(string(content))
|
||||||
|
combinedScript.WriteString("\n\n")
|
||||||
|
|
||||||
|
common.SysLog("Loaded script: " + filepath.Base(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存合并后的脚本
|
||||||
|
p.scripts = combinedScript.String()
|
||||||
|
|
||||||
|
// 执行脚本
|
||||||
|
if p.scripts != "" {
|
||||||
|
if _, err := vm.RunString(p.scripts); err != nil {
|
||||||
|
common.SysError("Failed to load combined scripts: " + err.Error())
|
||||||
|
} else {
|
||||||
|
common.SysLog("Successfully loaded and combined all JavaScript files from: " + scriptDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) ReloadScripts() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
// 清空缓存的脚本
|
||||||
|
p.scripts = ""
|
||||||
|
|
||||||
|
// 清空VM池,强制重新创建
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.pool:
|
||||||
|
default:
|
||||||
|
goto done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done:
|
||||||
|
common.SysLog("JavaScript scripts reloaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
func initJSRuntimePool() *JSRuntimePool {
|
||||||
|
jsPoolOnce.Do(func() {
|
||||||
|
jsRuntimePool = NewJSRuntimePool(jsConfig.MaxVMCount)
|
||||||
|
common.SysLog("JavaScript runtime pool initialized successfully")
|
||||||
|
})
|
||||||
|
return jsRuntimePool
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateGinContext(c *gin.Context) error {
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Errorf("gin context is nil")
|
||||||
|
}
|
||||||
|
if c.Request == nil {
|
||||||
|
return fmt.Errorf("gin context request is nil")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) executeWithTimeout(_ *goja.Runtime, fn func() (goja.Value, error)) (goja.Value, error) {
|
||||||
|
type result struct {
|
||||||
|
value goja.Value
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChan := make(chan result, 1)
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
resultChan <- result{err: fmt.Errorf("JS panic: %v", r)}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
value, err := fn()
|
||||||
|
resultChan <- result{value: value, err: err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case res := <-resultChan:
|
||||||
|
return res.value, res.err
|
||||||
|
case <-time.After(jsConfig.ScriptTimeout):
|
||||||
|
return nil, fmt.Errorf("script execution timeout after %v", jsConfig.ScriptTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) PreProcessRequest(c *gin.Context) error {
|
||||||
|
if err := validateGinContext(c); err != nil {
|
||||||
|
common.SysError("JS PreProcess Validation Error: " + err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vm := p.Get()
|
||||||
|
defer p.Put(vm)
|
||||||
|
|
||||||
|
preProcessFunc := vm.Get("preProcessRequest")
|
||||||
|
if preProcessFunc == nil || goja.IsUndefined(preProcessFunc) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsReq, err := common.StructToMap(createJSReq(c))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create JS context: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.executeWithTimeout(vm, func() (goja.Value, error) {
|
||||||
|
fn, ok := goja.AssertFunction(preProcessFunc)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("preProcessRequest is not a function")
|
||||||
|
}
|
||||||
|
return fn(goja.Undefined(), vm.ToValue(jsReq))
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("JS PreProcess Error: " + err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理返回结果
|
||||||
|
if result != nil && !goja.IsUndefined(result) {
|
||||||
|
resultObj := result.Export()
|
||||||
|
if resultMap, ok := resultObj.(map[string]any); ok {
|
||||||
|
// 是否修改请求
|
||||||
|
if newBody, exists := resultMap["body"]; exists {
|
||||||
|
switch v := newBody.(type) {
|
||||||
|
case string:
|
||||||
|
c.Request.Body = io.NopCloser(strings.NewReader(v))
|
||||||
|
c.Request.ContentLength = int64(len(v))
|
||||||
|
case []byte:
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(v))
|
||||||
|
c.Request.ContentLength = int64(len(v))
|
||||||
|
case map[string]any:
|
||||||
|
bodyBytes, err := json.Marshal(v)
|
||||||
|
if err == nil {
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
c.Request.ContentLength = int64(len(bodyBytes))
|
||||||
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
} else {
|
||||||
|
common.SysError("JS PreProcess JSON Marshal Error: " + err.Error())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
common.SysError("JS PreProcess Unsupported Body Type: " + fmt.Sprintf("%T", newBody))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否修改 headers
|
||||||
|
if newHeaders, exists := resultMap["headers"]; exists {
|
||||||
|
if headersMap, ok := newHeaders.(map[string]any); ok {
|
||||||
|
for key, value := range headersMap {
|
||||||
|
if valueStr, ok := value.(string); ok {
|
||||||
|
c.Request.Header.Set(key, valueStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否阻止请求
|
||||||
|
if block, exists := resultMap["block"]; exists {
|
||||||
|
if blockBool, ok := block.(bool); ok && blockBool {
|
||||||
|
status := http.StatusForbidden
|
||||||
|
if statusCode, exists := resultMap["statusCode"]; exists {
|
||||||
|
if statusInt, ok := statusCode.(float64); ok {
|
||||||
|
status = int(statusInt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message := "Request blocked by pre-process script"
|
||||||
|
if msg, exists := resultMap["message"]; exists {
|
||||||
|
if msgStr, ok := msg.(string); ok {
|
||||||
|
message = msgStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(status, gin.H{"error": message})
|
||||||
|
c.Abort()
|
||||||
|
return fmt.Errorf("request blocked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) PostProcessResponse(c *gin.Context, statusCode int, body []byte) (int, []byte, error) {
|
||||||
|
if err := validateGinContext(c); err != nil {
|
||||||
|
common.SysError("JS PostProcess Validation Error: " + err.Error())
|
||||||
|
return statusCode, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vm := p.Get()
|
||||||
|
defer p.Put(vm)
|
||||||
|
|
||||||
|
postProcessFunc := vm.Get("postProcessResponse")
|
||||||
|
if postProcessFunc == nil || goja.IsUndefined(postProcessFunc) {
|
||||||
|
return statusCode, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsReq, err := common.StructToMap(createJSReq(c))
|
||||||
|
if err != nil {
|
||||||
|
return statusCode, body, fmt.Errorf("failed to create JS context: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsResp := &JSResponse{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
Headers: make(map[string]string),
|
||||||
|
Body: string(body),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取响应头
|
||||||
|
if c.Writer != nil {
|
||||||
|
for key, values := range c.Writer.Header() {
|
||||||
|
if len(values) > 0 {
|
||||||
|
jsResp.Headers[key] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsResponse, err := common.StructToMap(jsResp)
|
||||||
|
if err != nil {
|
||||||
|
return statusCode, body, fmt.Errorf("failed to create JS response context: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.executeWithTimeout(vm, func() (goja.Value, error) {
|
||||||
|
fn, ok := goja.AssertFunction(postProcessFunc)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("postProcessResponse is not a function")
|
||||||
|
}
|
||||||
|
return fn(goja.Undefined(), vm.ToValue(jsReq), vm.ToValue(jsResponse))
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("JS PostProcess Error: " + err.Error())
|
||||||
|
return statusCode, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理返回
|
||||||
|
if result != nil && !goja.IsUndefined(result) {
|
||||||
|
resultObj := result.Export()
|
||||||
|
if resultMap, ok := resultObj.(map[string]any); ok {
|
||||||
|
if newStatusCode, exists := resultMap["statusCode"]; exists {
|
||||||
|
if statusInt, ok := newStatusCode.(float64); ok {
|
||||||
|
statusCode = int(statusInt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newBody, exists := resultMap["body"]; exists {
|
||||||
|
if bodyStr, ok := newBody.(string); ok {
|
||||||
|
body = []byte(bodyStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newHeaders, exists := resultMap["headers"]; exists {
|
||||||
|
if headersMap, ok := newHeaders.(map[string]any); ok {
|
||||||
|
for key, value := range headersMap {
|
||||||
|
if valueStr, ok := value.(string); ok {
|
||||||
|
c.Header(key, valueStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusCode, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *JSRuntimePool) hasPostProcessFunction() bool {
|
||||||
|
vm := p.Get()
|
||||||
|
defer p.Put(vm)
|
||||||
|
postProcessFunc := vm.Get("postProcessResponse")
|
||||||
|
return postProcessFunc != nil && !goja.IsUndefined(postProcessFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func JSRuntimeMiddleware() *gin.HandlerFunc {
|
||||||
|
loadCfg()
|
||||||
|
if !jsConfig.Enabled {
|
||||||
|
common.SysLog("JavaScript Runtime is disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := initJSRuntimePool()
|
||||||
|
var fn gin.HandlerFunc
|
||||||
|
fn = func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// 预处理
|
||||||
|
if err := pool.PreProcessRequest(c); err != nil {
|
||||||
|
common.SysError("JS Runtime PreProcess Error: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
if duration > time.Millisecond*100 {
|
||||||
|
common.SysLog(fmt.Sprintf("JS Runtime PreProcess took %v", duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后处理
|
||||||
|
if pool.hasPostProcessFunction() {
|
||||||
|
writer := newResponseWriter(c.Writer)
|
||||||
|
c.Writer = writer
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 后处理响应
|
||||||
|
if writer.body.Len() > 0 {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
statusCode, body, err := pool.PostProcessResponse(c, writer.statusCode, writer.body.Bytes())
|
||||||
|
if err == nil {
|
||||||
|
c.Writer = writer.ResponseWriter
|
||||||
|
|
||||||
|
for k, v := range writer.headerMap {
|
||||||
|
for _, value := range v {
|
||||||
|
c.Writer.Header().Add(k, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(statusCode)
|
||||||
|
|
||||||
|
if len(body) >= 0 {
|
||||||
|
c.Writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||||
|
c.Writer.Write(body)
|
||||||
|
} else {
|
||||||
|
c.Writer.Header().Del("Content-Length")
|
||||||
|
c.Writer.Write(body)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 出错时回复原响应
|
||||||
|
c.Writer = writer.ResponseWriter
|
||||||
|
c.Status(writer.statusCode)
|
||||||
|
|
||||||
|
common.SysError(fmt.Sprintf("JS Runtime PostProcess Error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
if duration > time.Millisecond*100 {
|
||||||
|
common.SysLog(fmt.Sprintf("JS Runtime PostProcess took %v", duration))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有响应体时,恢复原始writer
|
||||||
|
c.Writer = writer.ResponseWriter
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReloadJSScripts() {
|
||||||
|
if jsRuntimePool != nil {
|
||||||
|
jsRuntimePool.ReloadScripts()
|
||||||
|
common.SysLog("JavaScript scripts reloaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
139
middleware/jsrt/req.go
Normal file
139
middleware/jsrt/req.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package jsrt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 请求
|
||||||
|
type JSReq struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
Body any `json:"body"`
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
|
RemoteIP string `json:"remoteIP"`
|
||||||
|
Extra map[string]any `json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSResponse struct {
|
||||||
|
StatusCode int `json:"statusCode"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseWriter struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
body *bytes.Buffer
|
||||||
|
statusCode int
|
||||||
|
headerMap http.Header
|
||||||
|
written bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func createJSReq(c *gin.Context) *JSReq {
|
||||||
|
var bodyBytes []byte
|
||||||
|
if c.Request != nil && c.Request.Body != nil {
|
||||||
|
bodyBytes, _ = io.ReadAll(c.Request.Body)
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// headers map
|
||||||
|
headers := make(map[string]string)
|
||||||
|
if c.Request != nil && c.Request.Header != nil {
|
||||||
|
for key, values := range c.Request.Header {
|
||||||
|
if len(values) > 0 {
|
||||||
|
headers[key] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
method := ""
|
||||||
|
url := ""
|
||||||
|
userAgent := ""
|
||||||
|
remoteIP := ""
|
||||||
|
contentType := ""
|
||||||
|
|
||||||
|
if c.Request != nil {
|
||||||
|
method = c.Request.Method
|
||||||
|
if c.Request.URL != nil {
|
||||||
|
url = c.Request.URL.String()
|
||||||
|
}
|
||||||
|
userAgent = c.Request.UserAgent()
|
||||||
|
contentType = c.ContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c != nil {
|
||||||
|
remoteIP = c.ClientIP()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedBody := parseBodyByType(bodyBytes, contentType)
|
||||||
|
|
||||||
|
return &JSReq{
|
||||||
|
Method: method,
|
||||||
|
URL: url,
|
||||||
|
Headers: headers,
|
||||||
|
Body: parsedBody,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
RemoteIP: remoteIP,
|
||||||
|
Extra: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResponseWriter(w gin.ResponseWriter) *responseWriter {
|
||||||
|
return &responseWriter{
|
||||||
|
ResponseWriter: w,
|
||||||
|
body: &bytes.Buffer{},
|
||||||
|
statusCode: 200,
|
||||||
|
headerMap: make(http.Header),
|
||||||
|
written: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Write(data []byte) (int, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if !w.written {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
|
return w.body.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) WriteString(s string) (int, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if !w.written {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
|
return w.body.WriteString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) WriteHeader(statusCode int) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if w.written {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.statusCode = statusCode
|
||||||
|
w.written = true
|
||||||
|
|
||||||
|
maps.Copy(w.headerMap, w.ResponseWriter.Header())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Header() http.Header {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
|
||||||
|
if w.headerMap == nil {
|
||||||
|
w.headerMap = make(http.Header)
|
||||||
|
}
|
||||||
|
return w.headerMap
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user